fix(bot): stop reconnect loop during fresh pairing — root cause of QR rotation every 5s

The session-manager's auto-reconnect (5 s after a non-logged-out close)
was firing during initial pairing. Baileys closes the socket whenever it
exhausts its QR refs (or transient handshake errors); the auto-reconnect
then opened a brand-new socket → new QR pool → another close 5 s later.
The web saw a fresh QR every ~5 s and the user could never link, because
WhatsApp invalidates each QR as soon as Baileys cycles to the next.

Fix: only auto-reconnect for accounts that have been linked before
(`whatsapp_accounts.last_connected_at IS NOT NULL`). For brand-new
pairing attempts the pair-handler's 5-minute window is now the single
authority; on close we just stop the session and let the operator
retry. With auto-reconnect off, Baileys uses its default QR cadence:
60 s for the first QR, 20 s for each subsequent rotation, ~6 refs total
(~3 minutes of valid scanning) — plenty of time to scan.

Pair-handler now also surfaces ANY close as `session.timeout` to the
web (was only emitting on `loggedOut`). Without this the user would be
left staring at the last QR after Baileys gives up, with no way to know
pairing failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
yiekheng 2026-05-10 08:45:47 +08:00
parent 7b4f0d0b84
commit 4d10c72551
2 changed files with 29 additions and 8 deletions

View File

@ -128,7 +128,12 @@ export async function handleStartPairing(accountId: string): Promise<void> {
count: synced,
});
off();
} else if (event.type === "close" && event.loggedOut) {
} else if (event.type === "close") {
// During the pairing window, ANY close means the QR window
// ended without a successful link — Baileys' default is to
// close after exhausting QR refs (~2.5 min). Surface this to
// the UI so the user gets a "pairing timed out" screen and a
// chance to retry, instead of staring at a stale QR forever.
const t = pairTimeouts.get(id);
if (t) {
clearTimeout(t);

View File

@ -141,14 +141,30 @@ class SessionManager {
.set({ status: event.loggedOut ? "logged_out" : "disconnected" })
.where(eq(whatsappAccounts.id, accountId));
if (!event.loggedOut) {
const timer = setTimeout(() => {
this.reconnectTimers.delete(accountId);
void this.stop(accountId).then(() => this.start(accountId));
}, 5000);
this.reconnectTimers.set(accountId, timer);
} else {
if (event.loggedOut) {
await this.stop(accountId);
} else {
// Only auto-reconnect for accounts that have been linked at least
// once — `lastConnectedAt` is set on `open`. During an initial
// pairing attempt the close event fires every time Baileys
// exhausts QR refs (~every 30s). Reconnecting would restart the
// pair dance and rotate the QR every few seconds — pair-handler
// already manages the pairing window via its own 5-min timeout.
const account = await db.query.whatsappAccounts.findFirst({
where: (a, { eq }) => eq(a.id, accountId),
columns: { lastConnectedAt: true },
});
if (account?.lastConnectedAt) {
const timer = setTimeout(() => {
this.reconnectTimers.delete(accountId);
void this.stop(accountId).then(() => this.start(accountId));
}, 5000);
this.reconnectTimers.set(accountId, timer);
} else {
// Brand-new account that hasn't authenticated yet — let the
// pair-handler clean up via its timeout.
await this.stop(accountId);
}
}
} else if (event.type === "qr") {
await db