fix: 收紧严格发送响应判定
This commit is contained in:
parent
9c1454b16a
commit
0517586c53
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -196,35 +196,45 @@ describe('QQBot strict plain-text sender', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects OneBot non-success responses permanently and retains the pending log', async () => {
|
||||
const { messageService, reverseWs, sendLogRepository, service } =
|
||||
createHarness();
|
||||
reverseWs.sendAction.mockResolvedValue({
|
||||
message: 'bad target',
|
||||
retcode: 1404,
|
||||
status: 'failed',
|
||||
});
|
||||
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',
|
||||
...response,
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.sendStrictPlainText({
|
||||
attemptNumber: 1,
|
||||
deliveryId: 'delivery-1',
|
||||
message: 'plain text',
|
||||
selfId: '10001',
|
||||
targetId: '20001',
|
||||
targetType: 'group',
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: 'onebot_rejected',
|
||||
retryable: false,
|
||||
sendLogId: 'log-1',
|
||||
});
|
||||
expect(sendLogRepository.update).toHaveBeenCalledWith(
|
||||
{ id: 'log-1' },
|
||||
expect.objectContaining({ errorMessage: 'bad target', status: 'failed' }),
|
||||
);
|
||||
expect(messageService.saveOutgoing).not.toHaveBeenCalled();
|
||||
});
|
||||
await expect(
|
||||
service.sendStrictPlainText({
|
||||
attemptNumber: 1,
|
||||
deliveryId: 'delivery-1',
|
||||
message: 'plain text',
|
||||
selfId: '10001',
|
||||
targetId: '20001',
|
||||
targetType: 'group',
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: 'onebot_rejected',
|
||||
retryable: false,
|
||||
sendLogId: 'log-1',
|
||||
});
|
||||
expect(sendLogRepository.update).toHaveBeenCalledWith(
|
||||
{ id: 'log-1' },
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user