diff --git a/packages/onebot/src/event-converter/to-message.ts b/packages/onebot/src/event-converter/to-message.ts index d6c48ec6..49ef9954 100644 --- a/packages/onebot/src/event-converter/to-message.ts +++ b/packages/onebot/src/event-converter/to-message.ts @@ -73,6 +73,7 @@ export async function convertGroupMessage(ctx: ConverterContext, event: GroupMes message_id: messageId, message_seq: event.msgSeq, group_id: event.groupId, + group_name: event.groupName, user_id: event.senderUin, message: segments, raw_message: segmentsToRawMessage(segments), diff --git a/packages/onebot/tests/event-converter.test.ts b/packages/onebot/tests/event-converter.test.ts index 3ffc10ec..59475cec 100644 --- a/packages/onebot/tests/event-converter.test.ts +++ b/packages/onebot/tests/event-converter.test.ts @@ -54,6 +54,7 @@ function makeGroupMessage(senderUin: number): GroupMessage { time: 1700000000, selfUin: SELF_ID, groupId: 99999, + groupName: 'Test Group 测试群', senderUin, senderNick: senderUin === SELF_ID ? 'me' : 'peer', senderCard: '', @@ -100,6 +101,7 @@ describe('convertEvent — message kinds', () => { expect(out!.message_type).toBe('group'); expect(out!.sub_type).toBe('normal'); expect(out!.group_id).toBe(GROUP_ID); + expect(out!.group_name).toBe('Test Group 测试群'); }); it('group_message self → post_type "message_sent"', async () => { diff --git a/packages/protocol/src/events.ts b/packages/protocol/src/events.ts index dade5237..c37a42b3 100644 --- a/packages/protocol/src/events.ts +++ b/packages/protocol/src/events.ts @@ -133,6 +133,10 @@ export interface FriendMessage extends QQEvent { export interface GroupMessage extends QQEvent { kind: 'group_message'; groupId: number; + /** Group display name. From the message's own wire field (ResponseGrp + * field 7 — accurate even right after a rename), falling back to the + * identity group cache, else '' when neither is available. */ + groupName: string; senderUin: number; senderNick: string; senderCard: string; diff --git a/packages/protocol/src/msg-push/decoders/group-message.ts b/packages/protocol/src/msg-push/decoders/group-message.ts index a47c2bfe..2578bcf1 100644 --- a/packages/protocol/src/msg-push/decoders/group-message.ts +++ b/packages/protocol/src/msg-push/decoders/group-message.ts @@ -12,6 +12,7 @@ export const decodeGroupMessage: MsgPushDecoder = (ctx) => { msgId: ctx.head.msgId & 0x7FFFFFFF, elements: decodeRichBody(ctx.body, true), groupId: 0, + groupName: '', senderNick: '', senderCard: '', senderRole: '', @@ -43,5 +44,9 @@ export const decodeGroupMessage: MsgPushDecoder = (ctx) => { } else if (!ev.senderNick && grp?.memberCard) { ev.senderNick = grp.memberCard; } + // Group name: prefer the message's own wire field (ResponseGrp field 7 — + // fresh even right after a rename), fall back to the cached group list, else + // leave it empty. `ev.groupId` is set above from grp.groupUin. + ev.groupName = grp?.groupName || ctx.identity.findGroup(ev.groupId)?.groupName || ''; return [ev]; }; diff --git a/packages/protocol/tests/msg-push/fetch-group-history.test.ts b/packages/protocol/tests/msg-push/fetch-group-history.test.ts index c8815f9f..48ed6512 100644 --- a/packages/protocol/tests/msg-push/fetch-group-history.test.ts +++ b/packages/protocol/tests/msg-push/fetch-group-history.test.ts @@ -9,7 +9,7 @@ import type { SsoGetGroupMsg, SsoGetGroupMsgResponse } from '@snowluma/proto-def import type { IdentityService } from '../../src/identity-service'; import { SSO_GET_GROUP_MSG_CMD, fetchGroupMessageRange } from '../../src/msg-push'; -const identity = { findGroupMember: () => undefined } as unknown as IdentityService; +const identity = { findGroupMember: () => undefined, findGroup: () => null } as unknown as IdentityService; function okResult(data: Uint8Array): SendPacketResult { return { success: true, gotResponse: true, responseData: data } as SendPacketResult; @@ -69,7 +69,7 @@ describe('fetchGroupMessageRange / SsoGetGroupMsg', () => { it('[#1] prefers the fresher group card in grp.memberCard (field 4) over a stale cache card', async () => { const stale = { uin: 222, uid: '', nickname: 'BaseNick', card: 'StaleCard', role: 'member', level: 0, title: '', joinTime: 0, lastSentTime: 0, shutUpTime: 0 }; - const idWithMember = { findGroupMember: (_g: number, u: number) => (u === 222 ? stale : undefined) } as unknown as IdentityService; + const idWithMember = { findGroupMember: (_g: number, u: number) => (u === 222 ? stale : undefined), findGroup: () => null } as unknown as IdentityService; const resp = protobuf_encode({ body: { groupUin: 9999, messages: [ { responseHead: { fromUin: 222, grp: { groupUin: 9999, memberCard: 'FreshCard' } }, // field2 empty, field4 = fresh card @@ -84,7 +84,7 @@ describe('fetchGroupMessageRange / SsoGetGroupMsg', () => { it('[#1] keeps the cache card when field 4 equals the base nickname (no active group card)', async () => { const m = { uin: 222, uid: '', nickname: 'BaseNick', card: 'CachedCard', role: 'member', level: 0, title: '', joinTime: 0, lastSentTime: 0, shutUpTime: 0 }; - const id = { findGroupMember: () => m } as unknown as IdentityService; + const id = { findGroupMember: () => m, findGroup: () => null } as unknown as IdentityService; const resp = protobuf_encode({ body: { groupUin: 9999, messages: [ { responseHead: { fromUin: 222, grp: { groupUin: 9999, memberCard: 'BaseNick' } }, // field4 == base nick → not a card