fix: 摘要化 QQBot 图片消息日志

This commit is contained in:
sunlei 2026-06-05 20:34:03 +08:00
parent 2c833e83f2
commit 73280d379b
5 changed files with 146 additions and 7 deletions

View File

@ -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)}...<truncated ${text.length - maxLength} chars>`;
}
toStringId(value: number | string | undefined | null) {
return value === undefined || value === null ? '' : `${value}`;
}

View File

@ -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),

View File

@ -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<QqbotReverseActionSender> {
const { QqbotReverseWsService } = await import(
'../connection/qqbot-reverse-ws.service'
);
const { QqbotReverseWsService } =
await import('../connection/qqbot-reverse-ws.service');
return this.moduleRef.get<QqbotReverseActionSender>(QqbotReverseWsService, {
strict: false,
});
@ -212,4 +218,16 @@ export class QqbotSendService {
actionParams: { message: params.message, user_id: params.targetId },
};
}
private toStoredActionParams(
actionParams: Record<string, any>,
storedMessageText: string,
) {
return {
...actionParams,
...(actionParams.message === undefined
? {}
: { message: storedMessageText }),
};
}
}

View File

@ -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('<truncated 4000 chars>');
expect(stored.length).toBeLessThan(140);
});
});

View File

@ -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>]',
}),
);
});
});