@zhin.js/adapter-icqq 1.1.28 → 1.1.30
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 +70 -0
- package/README.md +59 -9
- package/adapters/icqq/index.js +3 -0
- package/adapters/icqq/index.ts +3 -0
- package/commands/icqq/login/code/index.js +41 -16
- package/commands/icqq/login/code/index.ts +37 -13
- package/commands/icqq/login/confirm/index.js +15 -9
- package/commands/icqq/login/confirm/index.ts +15 -6
- package/commands/icqq/login/definition.js +57 -3
- package/commands/icqq/login/definition.ts +62 -3
- package/commands/icqq/login/qr/index.js +14 -16
- package/commands/icqq/login/qr/index.ts +17 -15
- package/commands/icqq/login/status/index.js +32 -13
- package/commands/icqq/login/status/index.ts +30 -12
- package/lib/endpoint.d.ts +14 -1
- package/lib/endpoint.js +178 -41
- package/lib/i18n.d.ts +1 -1
- package/lib/i18n.js +283 -15
- package/lib/icqq-login-assist.d.ts +113 -20
- package/lib/icqq-login-assist.js +468 -97
- package/lib/icqq-login-control.d.ts +4 -1
- package/lib/icqq-login-descriptors.d.ts +48 -0
- package/lib/icqq-login-descriptors.js +187 -0
- package/lib/icqq-login-errors.d.ts +42 -0
- package/lib/icqq-login-errors.js +77 -0
- package/lib/sdk-logger.d.ts +14 -0
- package/lib/sdk-logger.js +44 -0
- package/package.json +45 -38
- package/plugin.js +0 -3
- package/src/endpoint.ts +176 -48
- package/src/i18n.ts +283 -15
- package/src/icqq-login-assist.ts +533 -114
- package/src/icqq-login-control.ts +4 -1
- package/src/icqq-login-descriptors.ts +200 -0
- package/src/icqq-login-errors.ts +112 -0
- package/src/sdk-logger.ts +57 -0
package/src/endpoint.ts
CHANGED
|
@@ -34,12 +34,15 @@ import {
|
|
|
34
34
|
} from 'zhin.js/adapter';
|
|
35
35
|
import type { EndpointContentPort } from '@zhin.js/adapter';
|
|
36
36
|
import { receiveOneBotLikeSideEvent } from '@zhin.js/adapter-onebot-events';
|
|
37
|
-
import { buildSystem, toCanonicalSegments, type LoginAssist, type LoginAssistEndpointIdentity, type PendingLoginTask } from '@zhin.js/core';
|
|
37
|
+
import { buildSystem, toCanonicalSegments, type LoginAssist, type LoginAssistEndpointIdentity, type LoginFlowSnapshot, type PendingLoginTask } from '@zhin.js/core';
|
|
38
38
|
import { EndpointDeliveryError, type EndpointSendPart, type EndpointSendResult, type MessageRef } from '@zhin.js/im-contract';
|
|
39
39
|
import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
|
|
40
40
|
import type { CapabilityId } from 'zhin.js';
|
|
41
41
|
import type { IcqqLoginControl } from './icqq-login-control.js';
|
|
42
|
-
import {
|
|
42
|
+
import { IcqqLoginDriver, icqqLinkHost, type IcqqChallengeKind } from './icqq-login-assist.js';
|
|
43
|
+
import { messages as t } from './i18n.js';
|
|
44
|
+
import { redactingIcqqLogger, type IcqqSdkLogger } from './sdk-logger.js';
|
|
45
|
+
import { sanitizeIcqqLoginMessage } from './icqq-login-errors.js';
|
|
43
46
|
import { IcqqContentResolver } from './content-resolver.js';
|
|
44
47
|
import {
|
|
45
48
|
InboundMessageDeduper,
|
|
@@ -113,6 +116,15 @@ const BOUND_EVENTS = [
|
|
|
113
116
|
'system.login.auth',
|
|
114
117
|
] as const;
|
|
115
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Log level of an ICQQ `system.*` event by meaning: coming online and login challenges waiting for
|
|
121
|
+
* the person (QR code, device lock, slider, identity check) are normal progress (`info`); only
|
|
122
|
+
* failures (login errors, going offline) are `warn`, so a routine login stays out of the warnings.
|
|
123
|
+
*/
|
|
124
|
+
export function icqqSystemEventLogLevel(type: string): 'info' | 'warn' {
|
|
125
|
+
return type === 'online' || type.startsWith('login.') && type !== 'login.error' ? 'info' : 'warn';
|
|
126
|
+
}
|
|
127
|
+
|
|
116
128
|
export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
117
129
|
readonly [endpointSpec]: EndpointSpecification;
|
|
118
130
|
readonly #logger: ReturnType<typeof getAdapterLogger>;
|
|
@@ -130,6 +142,7 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
130
142
|
#retirementEpoch = 0;
|
|
131
143
|
#retirementArmed = false;
|
|
132
144
|
#disposeNativeEvents?: () => void;
|
|
145
|
+
#disposeQrcodeError?: () => void;
|
|
133
146
|
#resolveStartOnline?: () => void;
|
|
134
147
|
#heldInbound: IcqqInboundMessage[] = [];
|
|
135
148
|
readonly #inflightInbound = new Set<Promise<void>>();
|
|
@@ -139,7 +152,15 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
139
152
|
readonly loginControl: IcqqLoginControl;
|
|
140
153
|
#reloginTimer?: ReturnType<typeof setTimeout>;
|
|
141
154
|
#reloginAttempts = 0;
|
|
155
|
+
/** Error code of the failure the automatic relogin is retrying (reported when it gives up). */
|
|
156
|
+
#lastAutoFailure: 'network' | 'unknown' = 'network';
|
|
142
157
|
#reloginInFlight?: Promise<void>;
|
|
158
|
+
/**
|
|
159
|
+
* Login flow of this Endpoint instance (ADR 0058). While a person answers a challenge no
|
|
160
|
+
* automatic relogin runs: a relogin starts a new SDK login, which issues a new challenge and
|
|
161
|
+
* voids the URL / ticket the operator is working on.
|
|
162
|
+
*/
|
|
163
|
+
readonly #login: IcqqLoginDriver;
|
|
143
164
|
#offlineNoticeAt = 0;
|
|
144
165
|
#challengeNoticeAt = 0;
|
|
145
166
|
|
|
@@ -246,12 +267,27 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
246
267
|
get transportState() { return state(); },
|
|
247
268
|
management: this.#management, control: this.#control, profile: createIcqqSelfProfile(this),
|
|
248
269
|
get content() { return content(); },
|
|
270
|
+
login: {
|
|
271
|
+
// The lease signal aborts on stop / retirement: never let a slow SDK call block it.
|
|
272
|
+
relogin: async ({ signal }) => { await (signal ? raceAbort(this.relogin(), signal) : this.relogin()); },
|
|
273
|
+
refreshChallenge: async ({ signal }) => {
|
|
274
|
+
await (signal ? raceAbort(this.refreshLoginChallenge(), signal) : this.refreshLoginChallenge());
|
|
275
|
+
},
|
|
276
|
+
},
|
|
249
277
|
});
|
|
278
|
+
// The SDK logs verification links (sid / sig) and the phone at `mark` level: redact them.
|
|
279
|
+
if (this.logger) this.logger = redactingIcqqLogger(this.logger as IcqqSdkLogger) as typeof this.logger;
|
|
250
280
|
this.#contentResolver = new IcqqContentResolver(this);
|
|
251
281
|
this.#content = this.#contentResolver.port;
|
|
252
282
|
this.#logger = getAdapterLogger('icqq', options.config.id);
|
|
253
283
|
this.#options = options;
|
|
254
284
|
this.endpointName = options.config.id;
|
|
285
|
+
this.#login = new IcqqLoginDriver({
|
|
286
|
+
assist: options.loginAssist, client: this, endpointKey: this.endpointName, owner: this.#loginAssistOwner,
|
|
287
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
288
|
+
...(options.endpointIdentity ? { endpointIdentity: options.endpointIdentity } : {}),
|
|
289
|
+
logger: this.#logger, ...(options.locale ? { locale: options.locale } : {}),
|
|
290
|
+
});
|
|
255
291
|
this.loginControl = Object.freeze({
|
|
256
292
|
endpointId: this.endpointName,
|
|
257
293
|
state: () => ({
|
|
@@ -261,6 +297,7 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
261
297
|
pending: this.#pendingLoginTasks(),
|
|
262
298
|
}),
|
|
263
299
|
pending: () => this.#pendingLoginTasks(),
|
|
300
|
+
flow: () => this.#loginFlow(),
|
|
264
301
|
relogin: async () => { this.#options.signal?.throwIfAborted(); await this.relogin(); },
|
|
265
302
|
submit: (taskId: string, value: string | Record<string, unknown>) => !this.#options.signal?.aborted && this.#options.loginAssist.submit(taskId, value, this.#options.endpointIdentity),
|
|
266
303
|
cancel: (taskId: string, reason?: string) => !this.#options.signal?.aborted && this.#options.loginAssist.cancel(taskId, reason, this.#options.endpointIdentity),
|
|
@@ -283,18 +320,28 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
283
320
|
this.emit('internal.error.network', -2, `ICQQ ${operation} failed`);
|
|
284
321
|
}
|
|
285
322
|
|
|
286
|
-
/** Force a login attempt (
|
|
323
|
+
/** Force a login attempt (Console「重新登录」, recovery command): a new login flow. */
|
|
287
324
|
relogin(): Promise<void> {
|
|
288
325
|
return this.#startRelogin(true);
|
|
289
326
|
}
|
|
290
327
|
|
|
328
|
+
/**
|
|
329
|
+
* Console「刷新二维码」: a new QR code in place while a QR challenge is shown; any other
|
|
330
|
+
* challenge cannot be refreshed alone, so this logs in again.
|
|
331
|
+
*/
|
|
332
|
+
async refreshLoginChallenge(): Promise<void> {
|
|
333
|
+
if (this.#stopped || this.#retiring || this.#options.signal?.aborted) throw new Error('ICQQ endpoint 已停止');
|
|
334
|
+
if (await this.#login.refresh()) return;
|
|
335
|
+
await this.relogin();
|
|
336
|
+
}
|
|
337
|
+
|
|
291
338
|
#startRelogin(resetAttempts: boolean): Promise<void> {
|
|
292
339
|
if (this.#stopped || this.#retiring) {
|
|
293
340
|
return Promise.reject(new Error('ICQQ endpoint 已停止'));
|
|
294
341
|
}
|
|
295
342
|
if (this.#reloginInFlight) return this.#reloginInFlight;
|
|
296
343
|
if (resetAttempts) this.#reloginAttempts = 0;
|
|
297
|
-
const task = this.#performRelogin();
|
|
344
|
+
const task = this.#performRelogin(resetAttempts);
|
|
298
345
|
this.#reloginInFlight = task;
|
|
299
346
|
void task.catch(() => undefined).finally(() => {
|
|
300
347
|
if (this.#reloginInFlight === task) this.#reloginInFlight = undefined;
|
|
@@ -305,10 +352,13 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
305
352
|
return task;
|
|
306
353
|
}
|
|
307
354
|
|
|
308
|
-
async #performRelogin(): Promise<void> {
|
|
355
|
+
async #performRelogin(manual: boolean): Promise<void> {
|
|
309
356
|
this.#clearReloginTimer();
|
|
310
357
|
this.cancelReconnect();
|
|
311
|
-
this.#logger.info(formatCompact({ op: 'icqq_relogin', endpoint: this.endpointName }));
|
|
358
|
+
this.#logger.info(formatCompact({ op: 'icqq_relogin', endpoint: this.endpointName, manual }));
|
|
359
|
+
// A requested relogin is a new attempt; an automatic retry continues the current timeline.
|
|
360
|
+
if (manual) this.#login.begin(t(this.#options.locale, 'loginReconnecting'));
|
|
361
|
+
else this.#login.connect(t(this.#options.locale, 'loginReconnecting'));
|
|
312
362
|
try {
|
|
313
363
|
await this.login(this.#options.config.password);
|
|
314
364
|
} catch (error) {
|
|
@@ -321,12 +371,33 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
321
371
|
}
|
|
322
372
|
}
|
|
323
373
|
|
|
324
|
-
|
|
374
|
+
/**
|
|
375
|
+
* Automatic backoff relogin, only for failures that need no verification (network drop, kicked,
|
|
376
|
+
* a plain login that did not finish). Never while a person answers a challenge (ADR 0058 §6).
|
|
377
|
+
*/
|
|
378
|
+
#scheduleRelogin(reason: 'kickoff' | 'login_network' | 'login_failed' | 'relogin_failed'): void {
|
|
325
379
|
if (!this.#options.config.autoReconnect || this.#stopped || this.#retiring) return;
|
|
380
|
+
if (this.#login.busy) return;
|
|
381
|
+
// A failed verification or an expired challenge waits for the person (next: relogin / refresh).
|
|
382
|
+
if (reason === 'relogin_failed' && this.#login.awaitingPerson) return;
|
|
326
383
|
if (this.#reloginTimer || this.#reloginInFlight) return;
|
|
327
|
-
if (this.#reloginAttempts >=
|
|
384
|
+
if (this.#reloginAttempts >= ICQQ_RELOGIN_ATTEMPTS) {
|
|
385
|
+
// Exhausted: the attempt ends and waits for the person.
|
|
386
|
+
if (!this.#login.awaitingPerson) {
|
|
387
|
+
const message = t(this.#options.locale, 'loginRetryExhausted', { attempts: ICQQ_RELOGIN_ATTEMPTS });
|
|
388
|
+
this.#login.fail(this.#lastAutoFailure, message, 'relogin');
|
|
389
|
+
this.#notifyOffline('relogin_exhausted', false, true);
|
|
390
|
+
}
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
328
393
|
const attempt = this.#reloginAttempts++;
|
|
329
394
|
const delayMs = Math.min(300_000, 5_000 * (2 ** Math.min(attempt, 5)));
|
|
395
|
+
const locale = this.#options.locale;
|
|
396
|
+
this.#login.info(t(locale, 'loginRetryScheduled', {
|
|
397
|
+
reason: t(locale, reason === 'kickoff' ? 'loginReasonKickoff' : reason === 'login_network' ? 'loginReasonNetwork' : 'loginReasonFailed'),
|
|
398
|
+
seconds: Math.round(delayMs / 1000),
|
|
399
|
+
attempt: attempt + 1,
|
|
400
|
+
}));
|
|
330
401
|
this.#logger.info(formatCompact({
|
|
331
402
|
op: 'icqq_relogin_scheduled',
|
|
332
403
|
endpoint: this.endpointName,
|
|
@@ -349,6 +420,17 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
349
420
|
this.#reloginTimer = undefined;
|
|
350
421
|
}
|
|
351
422
|
|
|
423
|
+
/** This incarnation's latest login flow (snapshot, including the active challenge's material). */
|
|
424
|
+
#loginFlow(): LoginFlowSnapshot | undefined {
|
|
425
|
+
if (this.#options.signal?.aborted) return undefined;
|
|
426
|
+
const flow = this.#options.loginAssist.getFlow('icqq', this.endpointName);
|
|
427
|
+
const identity = this.#options.endpointIdentity;
|
|
428
|
+
if (!flow || !identity) return flow;
|
|
429
|
+
const own = flow.endpointIdentity;
|
|
430
|
+
return own && own.canonicalId === identity.canonicalId && own.instanceId === identity.instanceId
|
|
431
|
+
&& own.epoch === identity.epoch && own.incarnationId === identity.incarnationId ? flow : undefined;
|
|
432
|
+
}
|
|
433
|
+
|
|
352
434
|
#pendingLoginTasks(): readonly PendingLoginTask[] {
|
|
353
435
|
if (this.#options.signal?.aborted) return [];
|
|
354
436
|
return this.#options.loginAssist.listPending()
|
|
@@ -360,11 +442,11 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
360
442
|
: task.endpointKey === this.endpointName));
|
|
361
443
|
}
|
|
362
444
|
|
|
363
|
-
#notifyOffline(reason: string, willRetry: boolean): void {
|
|
445
|
+
#notifyOffline(reason: string, willRetry: boolean, force = false): void {
|
|
364
446
|
const notifier = this.#options.notifier;
|
|
365
447
|
if (!notifier?.configured) return;
|
|
366
448
|
const now = Date.now();
|
|
367
|
-
if (now - this.#offlineNoticeAt < 60_000) return;
|
|
449
|
+
if (!force && now - this.#offlineNoticeAt < 60_000) return;
|
|
368
450
|
this.#offlineNoticeAt = now;
|
|
369
451
|
const recovery = willRetry
|
|
370
452
|
? '已开始自动重新登录。'
|
|
@@ -385,24 +467,28 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
385
467
|
});
|
|
386
468
|
}
|
|
387
469
|
|
|
388
|
-
#notifyChallenge(step:
|
|
470
|
+
#notifyChallenge(step: IcqqChallengeKind): void {
|
|
389
471
|
const notifier = this.#options.notifier;
|
|
390
472
|
if (!notifier?.configured) return;
|
|
391
473
|
const now = Date.now();
|
|
392
474
|
if (now - this.#challengeNoticeAt < 30_000) return;
|
|
393
475
|
this.#challengeNoticeAt = now;
|
|
394
|
-
const
|
|
476
|
+
const id = this.endpointName;
|
|
477
|
+
const label = step === 'qrcode' ? '扫码登录'
|
|
395
478
|
: step === 'slider' ? '滑块验证'
|
|
396
|
-
: step === 'device' ? '
|
|
397
|
-
: '
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
479
|
+
: step === 'device' ? '设备锁验证'
|
|
480
|
+
: step === 'sms' ? '短信验证码'
|
|
481
|
+
: '身份验证';
|
|
482
|
+
const next = step === 'qrcode'
|
|
483
|
+
? `在 QQ 回复 \`icqq login qr ${id}\` 获取二维码,用手机 QQ 扫码并在手机上确认,登录会自动继续;没有继续时回复 \`icqq login confirm ${id}\`。`
|
|
484
|
+
: step === 'slider'
|
|
485
|
+
? `打开 Console 的 ICQQ 页面,用验证码辅助页完成滑块即可;也可以回复 \`icqq login status ${id}\` 查看滑块链接,再用 \`icqq login code ${id} <ticket>\` 提交。`
|
|
486
|
+
: step === 'device'
|
|
487
|
+
? `回复 \`icqq login status ${id}\` 查看验证链接,用手机完成验证后回复 \`icqq login confirm ${id}\`;改用短信验证码请回复 \`icqq login code ${id} sms\`。`
|
|
488
|
+
: step === 'sms'
|
|
489
|
+
? `收到短信后回复 \`icqq login code ${id} <6 位验证码>\`。`
|
|
490
|
+
: `需要复制设备信息到验证页,请打开 Console 的 ICQQ 页面按步骤完成;完成后点「已完成,继续登录」或回复 \`icqq login confirm ${id}\`。`;
|
|
491
|
+
void notifier.send(`[Zhin] ICQQ ${id} 需要${label}。\n${next}`).then((sent) => {
|
|
406
492
|
if (sent) return;
|
|
407
493
|
this.#logger.warn(formatCompact({
|
|
408
494
|
op: 'icqq_notify_failed',
|
|
@@ -461,6 +547,7 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
461
547
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
462
548
|
try {
|
|
463
549
|
this.#bindClientEvents();
|
|
550
|
+
this.#login.begin();
|
|
464
551
|
await raceAbort(Promise.resolve(this.login(this.#options.config.password)), signal);
|
|
465
552
|
await onlineReady;
|
|
466
553
|
signal.throwIfAborted();
|
|
@@ -507,6 +594,7 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
507
594
|
this.#assertExternalLifecycle('stop');
|
|
508
595
|
this.#stopped = true;
|
|
509
596
|
this.#clearReloginTimer();
|
|
597
|
+
this.#login.invalidate();
|
|
510
598
|
this.#armRetirementFence();
|
|
511
599
|
await Promise.allSettled([...this.#inflightInbound]);
|
|
512
600
|
this.#resolveStartOnline?.();
|
|
@@ -514,6 +602,8 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
514
602
|
this.#heldInbound.length = 0;
|
|
515
603
|
this.#disposeNativeEvents?.();
|
|
516
604
|
this.#disposeNativeEvents = undefined;
|
|
605
|
+
this.#disposeQrcodeError?.();
|
|
606
|
+
this.#disposeQrcodeError = undefined;
|
|
517
607
|
for (const event of BOUND_EVENTS) {
|
|
518
608
|
// The retirement guard owns this matcher until a late login handshake
|
|
519
609
|
// has finished. start()/open() replace it with the normal listener.
|
|
@@ -812,7 +902,7 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
812
902
|
this.#scheduleRelogin('kickoff');
|
|
813
903
|
});
|
|
814
904
|
this.on('system.login.qrcode', (event) => {
|
|
815
|
-
this.#logger.info('icqq
|
|
905
|
+
this.#logger.info('icqq 收到二维码登录事件,等待扫码(Console / 终端 / QQ 命令)');
|
|
816
906
|
this.#handleLoginChallenge('qrcode', event);
|
|
817
907
|
});
|
|
818
908
|
this.on('system.login.device', (event) => {
|
|
@@ -822,11 +912,22 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
822
912
|
this.#handleLoginChallenge('slider', event);
|
|
823
913
|
});
|
|
824
914
|
this.on('system.login.error', (event) => {
|
|
825
|
-
|
|
826
|
-
this.#logSystemEvent('login.error', event
|
|
827
|
-
|
|
828
|
-
|
|
915
|
+
const raw = event && typeof event === 'object' ? event as { code?: unknown; message?: unknown } : {};
|
|
916
|
+
this.#logSystemEvent('login.error', event, {
|
|
917
|
+
...(typeof raw.code === 'number' ? { code: raw.code } : {}),
|
|
918
|
+
message: sanitizeIcqqLoginMessage(raw.message),
|
|
919
|
+
});
|
|
920
|
+
if (this.#retiring || this.#stopped || this.#options.signal?.aborted) return;
|
|
921
|
+
this.#handleLoginError(event);
|
|
829
922
|
});
|
|
923
|
+
// The SDK's second scan query (inside qrcodeLogin) failed: report it instead of verifying forever.
|
|
924
|
+
// Only our listener is removed on stop (the SDK has its own one on this internal event).
|
|
925
|
+
this.#disposeQrcodeError?.();
|
|
926
|
+
const disposeQrcodeError = this.on('internal.error.qrcode', (code: unknown, message: unknown) => {
|
|
927
|
+
if (this.#retiring || this.#stopped || this.#options.signal?.aborted) return;
|
|
928
|
+
this.#login.qrcodeError(code, message);
|
|
929
|
+
});
|
|
930
|
+
this.#disposeQrcodeError = () => { disposeQrcodeError(); };
|
|
830
931
|
this.on('system.login.auth', (event) => {
|
|
831
932
|
this.#handleLoginChallenge('auth', event);
|
|
832
933
|
});
|
|
@@ -836,12 +937,12 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
836
937
|
this.on('system.online', (event) => {
|
|
837
938
|
if (this.#retiring) return;
|
|
838
939
|
this.#everOnline = true;
|
|
940
|
+
this.#login.online();
|
|
839
941
|
this.#clearReloginTimer();
|
|
840
942
|
this.#reloginAttempts = 0;
|
|
841
943
|
this.#notifyRecovered();
|
|
842
944
|
try {
|
|
843
945
|
this.#resolveStartOnline?.();
|
|
844
|
-
this.#options.loginAssist.cancelOwned(this.#loginAssistOwner, 'online');
|
|
845
946
|
try {
|
|
846
947
|
// transport 重连不清除(账号不变);stop/retire 由基类撤销登记。上报失败不影响上线流程。
|
|
847
948
|
this[endpointSpec].reportSelfUserId(resolveIcqqSelfUserId(this));
|
|
@@ -894,37 +995,56 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
894
995
|
});
|
|
895
996
|
}
|
|
896
997
|
|
|
897
|
-
#handleLoginChallenge(
|
|
898
|
-
step: 'qrcode' | 'slider' | 'device' | 'auth',
|
|
899
|
-
event: unknown,
|
|
900
|
-
): void {
|
|
998
|
+
#handleLoginChallenge(step: Exclude<IcqqChallengeKind, 'sms'>, event: unknown): void {
|
|
901
999
|
if (this.#retiring || this.#stopped || this.#options.signal?.aborted) return;
|
|
902
1000
|
const payload = serializeIcqqEvent(event) ?? (event && typeof event === 'object'
|
|
903
1001
|
? event as Record<string, unknown>
|
|
904
1002
|
: null);
|
|
905
|
-
|
|
1003
|
+
// Verification links carry a session id (sid / sig): logs get the challenge kind and host only.
|
|
1004
|
+
this.#logSystemEvent(`login.${step}`, event, loginChallengeLogLine(step, payload));
|
|
906
1005
|
this.#clearReloginTimer();
|
|
907
1006
|
this.#notifyChallenge(step);
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
client: this,
|
|
912
|
-
adapter: 'icqq',
|
|
913
|
-
endpointKey: this.endpointName,
|
|
914
|
-
owner: this.#loginAssistOwner,
|
|
915
|
-
signal: this.#options.signal, endpointIdentity: this.#options.endpointIdentity,
|
|
916
|
-
logger: this.#logger,
|
|
917
|
-
step,
|
|
918
|
-
event: payload ?? event,
|
|
919
|
-
});
|
|
1007
|
+
// The QR image is a Buffer, which serialization drops; the driver reads the raw event.
|
|
1008
|
+
const raw = event && typeof event === 'object' ? event as Record<string, unknown> : {};
|
|
1009
|
+
this.#login.challenge(step, { ...(payload ?? {}), ...raw });
|
|
920
1010
|
}
|
|
921
1011
|
|
|
922
|
-
|
|
923
|
-
|
|
1012
|
+
/**
|
|
1013
|
+
* `system.login.error` (ADR 0058 §5–6): a wrong code stays on its challenge; a verification
|
|
1014
|
+
* failure ends the attempt and waits for the person (`next`). Only a plain login that failed on
|
|
1015
|
+
* the network or for an unknown reason, without any challenge, keeps the automatic backoff.
|
|
1016
|
+
*/
|
|
1017
|
+
#handleLoginError(event: unknown): void {
|
|
1018
|
+
const { mapping, hadChallenge, retrying } = this.#login.loginError(event);
|
|
924
1019
|
this.#logger.warn(formatCompact({
|
|
1020
|
+
op: 'icqq_login_error', endpoint: this.endpointName, code: mapping.code,
|
|
1021
|
+
...(mapping.platformCode !== undefined ? { platform_code: mapping.platformCode } : {}),
|
|
1022
|
+
}));
|
|
1023
|
+
if (retrying) return;
|
|
1024
|
+
const autoRetry = this.#options.config.autoReconnect
|
|
1025
|
+
&& (mapping.code === 'network' || (mapping.code === 'unknown' && !hadChallenge));
|
|
1026
|
+
this.#notifyOffline('login_error', autoRetry);
|
|
1027
|
+
if (autoRetry) {
|
|
1028
|
+
this.#login.outcome({ result: 'rejected', code: mapping.code, message: mapping.message, next: 'wait' });
|
|
1029
|
+
this.#lastAutoFailure = mapping.code === 'network' ? 'network' : 'unknown';
|
|
1030
|
+
this.#scheduleRelogin(mapping.code === 'network' ? 'login_network' : 'login_failed');
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
this.#clearReloginTimer();
|
|
1034
|
+
this.#login.fail(mapping.code, mapping.message, mapping.next);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* `safe` replaces the payload in the log line and the `system.receive` side event (login
|
|
1039
|
+
* challenges keep only kind and host: links carry a sid, plus phone / device / QR image).
|
|
1040
|
+
*/
|
|
1041
|
+
#logSystemEvent(type: string, event: unknown, safe?: Record<string, unknown> | null): void {
|
|
1042
|
+
const payload = safe === undefined ? serializeIcqqEvent(event) : safe;
|
|
1043
|
+
const line = payload;
|
|
1044
|
+
this.#logger[icqqSystemEventLogLevel(type)](formatCompact({
|
|
925
1045
|
op: `system.${type}`,
|
|
926
1046
|
endpoint: this.endpointName,
|
|
927
|
-
...(
|
|
1047
|
+
...(line ? { event: JSON.stringify(line) } : {}),
|
|
928
1048
|
}));
|
|
929
1049
|
this.#dispatchIcqqSystemSideEvent(type, payload);
|
|
930
1050
|
}
|
|
@@ -1338,6 +1458,12 @@ function resolveNativeClientConfig(config: ResolvedIcqqConfig): NativeIcqqClient
|
|
|
1338
1458
|
};
|
|
1339
1459
|
}
|
|
1340
1460
|
|
|
1461
|
+
/** Log line of a login challenge: its kind and the link's host, never the link, image or device. */
|
|
1462
|
+
export function loginChallengeLogLine(kind: string, payload: Record<string, unknown> | null): Record<string, unknown> {
|
|
1463
|
+
const host = icqqLinkHost(payload?.url);
|
|
1464
|
+
return { kind, ...(host ? { host } : {}) };
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1341
1467
|
function serializeIcqqEvent(event: unknown): Record<string, unknown> | null {
|
|
1342
1468
|
if (!event || typeof event !== 'object') return null;
|
|
1343
1469
|
const nativeToJSON = (event as { toJSON?: (keys: string[]) => unknown }).toJSON;
|
|
@@ -1360,5 +1486,7 @@ function serializeIcqqEvent(event: unknown): Record<string, unknown> | null {
|
|
|
1360
1486
|
}
|
|
1361
1487
|
|
|
1362
1488
|
const INBOUND_HOLD_LIMIT = 32;
|
|
1489
|
+
/** Automatic backoff relogins before the attempt fails and waits for the person. */
|
|
1490
|
+
const ICQQ_RELOGIN_ATTEMPTS = 8;
|
|
1363
1491
|
|
|
1364
1492
|
const ICQQ_SERIALIZE_SKIP = ['group', 'member', 'friend', 'discuss', 'client'] as const;
|