fix: 收紧严格发送响应判定

This commit is contained in:
sunlei 2026-07-24 09:13:51 +08:00
parent 9c1454b16a
commit 0517586c53
3 changed files with 112 additions and 33 deletions

View File

@ -272,7 +272,9 @@ export class QqbotSendService {
input.action,
input.actionParams,
);
const success = response.status === 'ok' || response.retcode === 0;
const success = input.strict
? response.status === 'ok' && response.retcode === 0
: response.status === 'ok' || response.retcode === 0;
const messageId = response.data?.message_id
? `${response.data.message_id}`
: null;

View File

@ -145,6 +145,11 @@ export class QqbotReverseWsService
} catch {
clearTimeout(timer);
this.pendingActions.delete(echo);
this.closeCurrentConnection(
selfId,
ws,
'OneBot connection send failed',
);
reject(
new QqbotReverseWsActionError(
'onebot_disconnected',
@ -314,12 +319,25 @@ export class QqbotReverseWsService
}
/**
* QQBot
* @param selfId - ID
* @param ws - QQBot列表 closeTimedOutConnection
* Retires the current connection after an action timeout using the stable timeout reason.
* @param selfId - The account owning the timed-out OneBot action.
* @param ws - The exact socket that timed out.
*/
private closeTimedOutConnection(selfId: string, ws: WebSocket) {
const reason = 'OneBot action timeout';
this.closeCurrentConnection(selfId, ws, 'OneBot action timeout');
}
/**
* Retires exactly the currently registered failing connection and publishes its offline state.
* @param selfId - The account owning the attempted OneBot action.
* @param ws - The exact socket that failed; replaced sockets must remain registered.
* @param reason - A non-sensitive reason used for socket close and offline observation.
*/
private closeCurrentConnection(
selfId: string,
ws: WebSocket,
reason: string,
) {
let closedCurrentConnection = false;
[...this.connections.entries()].forEach(([key, connection]) => {
if (!key.startsWith(`${selfId}:`) || connection !== ws) return;

View File

@ -196,13 +196,19 @@ describe('QQBot strict plain-text sender', () => {
});
});
it('rejects OneBot non-success responses permanently and retains the pending log', async () => {
it.each([
['failed status despite zero retcode', { retcode: 0, status: 'failed' }],
['ok status with nonzero retcode', { retcode: 1404, status: 'ok' }],
['missing status', { retcode: 0 }],
['missing retcode', { status: 'ok' }],
])(
'rejects strict OneBot responses with %s permanently and retains the pending log',
async (_caseName, response) => {
const { messageService, reverseWs, sendLogRepository, service } =
createHarness();
reverseWs.sendAction.mockResolvedValue({
message: 'bad target',
retcode: 1404,
status: 'failed',
...response,
});
await expect(
@ -221,10 +227,14 @@ describe('QQBot strict plain-text sender', () => {
});
expect(sendLogRepository.update).toHaveBeenCalledWith(
{ id: 'log-1' },
expect.objectContaining({ errorMessage: 'bad target', status: 'failed' }),
expect.objectContaining({
errorMessage: 'bad target',
status: 'failed',
}),
);
expect(messageService.saveOutgoing).not.toHaveBeenCalled();
});
},
);
it.each([
[
@ -320,6 +330,11 @@ describe('QQBot strict plain-text sender', () => {
sendLogRepository,
service,
} = createHarness();
reverseWs.sendAction.mockResolvedValue({
data: { message_id: 'message-1' },
retcode: 0,
status: 'failed',
});
await service.sendText({
message: '[CQ:at,qq=12345]',
@ -343,6 +358,10 @@ describe('QQBot strict plain-text sender', () => {
expect(busService.publish).toHaveBeenCalledTimes(1);
expect(sendLogRepository.save).toHaveBeenCalledTimes(1);
expect(messageService.saveOutgoing).toHaveBeenCalledTimes(1);
expect(sendLogRepository.update).toHaveBeenCalledWith(
{ id: 'log-1' },
expect.objectContaining({ status: 'success' }),
);
});
});
@ -436,4 +455,44 @@ describe('QQBot reverse WS action classification', () => {
jest.useRealTimers();
}
});
it('retires an OPEN socket whose send throws without leaving a timeout behind', async () => {
jest.useFakeTimers();
try {
const { accountService, busService, service } = createReverseWsHarness();
const ws = {
close: jest.fn(),
readyState: 1,
send: jest.fn(() => {
throw new Error('send failed');
}),
};
(service as any).connections.set('10001:Universal', ws);
await expect(
service.sendAction('10001', 'send_group_msg', {}),
).rejects.toMatchObject({ code: 'onebot_disconnected' });
expect((service as any).pendingActions.size).toBe(0);
expect((service as any).connections.has('10001:Universal')).toBe(false);
expect(ws.close).toHaveBeenCalledWith(
1011,
'OneBot connection send failed',
);
expect(accountService.markOffline).toHaveBeenCalledWith(
'10001',
'OneBot connection send failed',
);
expect(busService.publish).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({ selfId: '10001', status: 'offline' }),
);
jest.advanceTimersByTime(10);
expect(ws.close).toHaveBeenCalledTimes(1);
expect(accountService.markOffline).toHaveBeenCalledTimes(1);
expect(busService.publish).toHaveBeenCalledTimes(1);
} finally {
jest.useRealTimers();
}
});
});