fix(channel): keep qqbot token refresh retrying (#5414)

This commit is contained in:
tt-a1i 2026-06-20 02:32:32 +08:00 committed by GitHub
parent c8e8c5b2df
commit 617eb20081
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 53 additions and 12 deletions

View file

@ -543,22 +543,26 @@ export class QQChannel extends ChannelBase {
process.stderr.write(
`[QQ:${this.name}] Token refresh failed: ${e}, retrying in 60s\n`,
);
// Retry fetchToken directly instead of going through
// scheduleTokenRefresh (which would add another ~60s of
// delay from the stale tokenExpiresAt).
this.tokenRefreshTimer = setTimeout(() => {
if (this.disposed) return;
this.fetchToken().catch(() => {
process.stderr.write(
`[QQ:${this.name}] Token refresh failed again after retry\n`,
);
});
}, 60_000);
this.scheduleTokenRefreshRetry();
});
}, delay);
}
}
private scheduleTokenRefreshRetry(): void {
if (this.disposed) return;
this.stopTokenRefresh();
this.tokenRefreshTimer = setTimeout(() => {
this.fetchToken().catch((e) => {
if (this.disposed) return;
process.stderr.write(
`[QQ:${this.name}] Token refresh failed: ${e}, retrying in 60s\n`,
);
this.scheduleTokenRefreshRetry();
});
}, 60_000);
}
private stopTokenRefresh(): void {
if (this.tokenRefreshTimer) {
clearTimeout(this.tokenRefreshTimer);

View file

@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { isValidChatId, hasMarkdownSyntax, splitText } from './QQChannel.js';
const {
@ -106,6 +106,10 @@ const { QQChannel } = await import('./QQChannel.js');
type QQChannelOptions = ConstructorParameters<typeof QQChannel>[3];
type QQChannelRouter = NonNullable<QQChannelOptions>['router'];
afterEach(() => {
vi.useRealTimers();
});
/** Create a mock Response-like object for sendQQMessage. */
function mockResponse(
ok: boolean,
@ -471,6 +475,39 @@ describe('sendMessage', () => {
expect(mockFetchAccessToken).toHaveBeenCalled();
});
it('keeps retrying scheduled token refresh failures until one succeeds', async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-01-01T00:00:00Z'));
const ch = makeChannel();
const chp = ch as unknown as Record<string, unknown>;
chp['tokenExpiresAt'] = Date.now() + 120_000;
mockFetchAccessToken
.mockRejectedValueOnce(new Error('token endpoint down'))
.mockRejectedValueOnce(new Error('still down'))
.mockResolvedValueOnce({
accessToken: 'recovered-token',
expiresIn: 7200,
});
(chp['scheduleTokenRefresh'] as () => void).call(ch);
await vi.advanceTimersByTimeAsync(60_000);
expect(mockFetchAccessToken).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(60_000);
expect(mockFetchAccessToken).toHaveBeenCalledTimes(2);
await vi.advanceTimersByTimeAsync(60_000);
expect(mockFetchAccessToken).toHaveBeenCalledTimes(3);
expect(chp['accessToken']).toBe('recovered-token');
await vi.advanceTimersByTimeAsync(60_000);
expect(mockFetchAccessToken).toHaveBeenCalledTimes(3);
ch.disconnect();
});
it('catches thrown sendQQMessage errors and stops sending', async () => {
const ch = makeChannel({ chatType: 'c2c' });
mockSendQQMessage.mockRejectedValue(new Error('network down'));