From 73280d379b79534c6a9416babc59673268996194 Mon Sep 17 00:00:00 2001 From: sunlei Date: Fri, 5 Jun 2026 20:34:03 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=91=98=E8=A6=81=E5=8C=96=20QQBot=20?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E6=B6=88=E6=81=AF=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/services/tool.service.ts | 10 +++ src/qqbot/command/qqbot-command.service.ts | 19 +++++- src/qqbot/send/qqbot-send.service.ts | 30 +++++++-- test/common/tool.service.spec.ts | 21 +++++++ test/qqbot/send/qqbot-send.service.spec.ts | 73 ++++++++++++++++++++++ 5 files changed, 146 insertions(+), 7 deletions(-) create mode 100644 test/common/tool.service.spec.ts create mode 100644 test/qqbot/send/qqbot-send.service.spec.ts diff --git a/src/common/services/tool.service.ts b/src/common/services/tool.service.ts index d357fcf..327ad03 100644 --- a/src/common/services/tool.service.ts +++ b/src/common/services/tool.service.ts @@ -128,6 +128,16 @@ export class ToolsService { return this.toTrimmedString(value).replace(/\s+/g, ' '); } + toStoredMessageText(value: unknown, maxLength = 4000) { + const text = this.toTrimmedString(value).replace( + /\[CQ:image,file=base64:\/\/([A-Za-z0-9+/=]+)\]/g, + (_match, payload: string) => + `[CQ:image,file=base64://<${payload.length} chars>]`, + ); + if (text.length <= maxLength) return text; + return `${text.slice(0, maxLength)}...`; + } + toStringId(value: number | string | undefined | null) { return value === undefined || value === null ? '' : `${value}`; } diff --git a/src/qqbot/command/qqbot-command.service.ts b/src/qqbot/command/qqbot-command.service.ts index 4416ec7..0efd525 100644 --- a/src/qqbot/command/qqbot-command.service.ts +++ b/src/qqbot/command/qqbot-command.service.ts @@ -179,7 +179,9 @@ export class QqbotCommandService { input: JSON.stringify(params.input || {}), operationKey: params.command.operationKey, output: - params.output === undefined ? null : JSON.stringify(params.output), + params.output === undefined + ? null + : this.stringifyStoredOutput(params.output), pluginKey: params.command.pluginKey, rawMessage: params.message.messageText, selfId: params.message.selfId, @@ -288,6 +290,21 @@ export class QqbotCommandService { } } + private stringifyStoredOutput(output: any) { + if ( + output && + typeof output === 'object' && + !Array.isArray(output) && + typeof output.replyText === 'string' + ) { + return JSON.stringify({ + ...output, + replyText: this.toolsService.toStoredMessageText(output.replyText), + }); + } + return JSON.stringify(output); + } + private toRawBody(command: QqbotCommand): QqbotCommandBodyDto { return { aliases: this.parseList(command.aliases), diff --git a/src/qqbot/send/qqbot-send.service.ts b/src/qqbot/send/qqbot-send.service.ts index e59be16..b83127d 100644 --- a/src/qqbot/send/qqbot-send.service.ts +++ b/src/qqbot/send/qqbot-send.service.ts @@ -103,12 +103,19 @@ export class QqbotSendService { this.rateLimitService.assertCanSend(account.selfId, params.targetId); const { action, actionParams } = this.buildAction(params); + const storedMessageText = this.toolsService.toStoredMessageText( + params.message, + ); + const storedActionParams = this.toStoredActionParams( + actionParams, + storedMessageText, + ); const log = await this.sendLogRepository.save( this.sendLogRepository.create({ action, - messageText: params.message, - params: actionParams, + messageText: storedMessageText, + params: storedActionParams, selfId: account.selfId, status: 'pending', targetId: params.targetId, @@ -151,7 +158,7 @@ export class QqbotSendService { if (success) { await this.messageService.saveOutgoing({ messageId, - messageText: params.message, + messageText: storedMessageText, messageType: params.targetType, selfId: account.selfId, targetId: params.targetId, @@ -175,9 +182,8 @@ export class QqbotSendService { } private async getReverseWsService(): Promise { - const { QqbotReverseWsService } = await import( - '../connection/qqbot-reverse-ws.service' - ); + const { QqbotReverseWsService } = + await import('../connection/qqbot-reverse-ws.service'); return this.moduleRef.get(QqbotReverseWsService, { strict: false, }); @@ -212,4 +218,16 @@ export class QqbotSendService { actionParams: { message: params.message, user_id: params.targetId }, }; } + + private toStoredActionParams( + actionParams: Record, + storedMessageText: string, + ) { + return { + ...actionParams, + ...(actionParams.message === undefined + ? {} + : { message: storedMessageText }), + }; + } } diff --git a/test/common/tool.service.spec.ts b/test/common/tool.service.spec.ts new file mode 100644 index 0000000..ea9090c --- /dev/null +++ b/test/common/tool.service.spec.ts @@ -0,0 +1,21 @@ +import { ToolsService } from '@/common'; + +describe('ToolsService', () => { + const service = new ToolsService(); + + it('summarizes CQ image base64 payloads before storing message text', () => { + const stored = service.toStoredMessageText( + `before [CQ:image,file=base64://${'a'.repeat(70000)}] after`, + ); + + expect(stored).toBe('before [CQ:image,file=base64://<70000 chars>] after'); + expect(stored.length).toBeLessThan(100); + }); + + it('truncates oversized stored message text after image summarization', () => { + const stored = service.toStoredMessageText('x'.repeat(4100), 100); + + expect(stored).toContain(''); + expect(stored.length).toBeLessThan(140); + }); +}); diff --git a/test/qqbot/send/qqbot-send.service.spec.ts b/test/qqbot/send/qqbot-send.service.spec.ts new file mode 100644 index 0000000..e992a5c --- /dev/null +++ b/test/qqbot/send/qqbot-send.service.spec.ts @@ -0,0 +1,73 @@ +import { ToolsService } from '@/common'; +import { QqbotSendService } from '@/qqbot/send/qqbot-send.service'; + +describe('QqbotSendService', () => { + it('stores summarized CQ image payloads while sending the original message', async () => { + const originalMessage = `[CQ:image,file=base64://${'a'.repeat(70000)}]`; + const reverseWsService = { + sendAction: jest.fn().mockResolvedValue({ + data: { message_id: 'message-1' }, + retcode: 0, + status: 'ok', + }), + }; + const sendLogRepository = { + create: jest.fn((payload) => payload), + save: jest.fn(async (payload) => ({ ...payload, id: 'log-1' })), + update: jest.fn(), + }; + const messageService = { + saveOutgoing: jest.fn(), + }; + const busService = { + publish: jest.fn(), + }; + + const service = new QqbotSendService( + sendLogRepository as any, + { + getDefaultAccount: jest.fn().mockResolvedValue({ selfId: 'bot-1' }), + } as any, + busService as any, + messageService as any, + { get: jest.fn(() => reverseWsService) } as any, + { assertCanSend: jest.fn() } as any, + new ToolsService(), + ); + (service as any).getReverseWsService = jest + .fn() + .mockResolvedValue(reverseWsService); + + await service.sendText({ + message: originalMessage, + selfId: 'bot-1', + targetId: 'group-1', + targetType: 'group', + }); + + expect(sendLogRepository.save).toHaveBeenCalledWith( + expect.objectContaining({ + messageText: '[CQ:image,file=base64://<70000 chars>]', + params: expect.objectContaining({ + message: '[CQ:image,file=base64://<70000 chars>]', + }), + }), + ); + expect(busService.publish).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + params: expect.objectContaining({ message: originalMessage }), + }), + ); + expect(reverseWsService.sendAction).toHaveBeenCalledWith( + 'bot-1', + 'send_group_msg', + expect.objectContaining({ message: originalMessage }), + ); + expect(messageService.saveOutgoing).toHaveBeenCalledWith( + expect.objectContaining({ + messageText: '[CQ:image,file=base64://<70000 chars>]', + }), + ); + }); +});