From 0ed85e77458268223e58770581e9038e90c229b0 Mon Sep 17 00:00:00 2001 From: DrSmoothl <1787882683@qq.com> Date: Thu, 30 Jul 2026 21:42:18 +0800 Subject: [PATCH] fix(onebot): report rejected group kick operations Treat group member removal as successful only when QQ accepts the operation. Preserve the rejection reason so callers no longer receive a false success when a target cannot be removed. Apply the same behavior to single-member and batch removal actions, while keeping accepted operations unchanged. Add regression coverage for both successful and rejected responses. Fixes #298 --- .../core/tests/actions/group-admin.test.ts | 31 +++++++++++++++++++ packages/proto-defs/src/oidb-actions/base.ts | 8 +++++ .../oidb-services/group-admin/kick-member.ts | 17 +++++++--- .../oidb-services/group-admin/kick-members.ts | 17 +++++++--- 4 files changed, 63 insertions(+), 10 deletions(-) diff --git a/packages/core/tests/actions/group-admin.test.ts b/packages/core/tests/actions/group-admin.test.ts index 6d263ee0..bbd48fc5 100644 --- a/packages/core/tests/actions/group-admin.test.ts +++ b/packages/core/tests/actions/group-admin.test.ts @@ -62,6 +62,9 @@ describe('apis/group-admin', () => { it('kickMember resolves UID per-group and forwards reject + reason', async () => { const bridge = mockBridge(); + bridge.sendRawPacket.mockResolvedValueOnce(packResponse( + Buffer.from('220308b960', 'hex'), + )); await new GroupAdminApi(bridge as any).kickMember(12345, 67890, true, 'bad behaviour'); expect(bridge.resolveUserUid).toHaveBeenCalledWith(67890, 12345); const [cmd, bytes] = bridge.sendRawPacket.mock.calls[0]!; @@ -75,6 +78,20 @@ describe('apis/group-admin', () => { }); }); + it('kickMember rejects a server business error instead of reporting success (#298)', async () => { + const bridge = mockBridge(); + bridge.sendRawPacket.mockResolvedValueOnce(packResponse( + Buffer.from( + '222008b960121be7bea4e4b8bbe697a0e6b395e8a2abe7a7bbe587bae7bea4e8818a', + 'hex', + ), + )); + + await expect( + new GroupAdminApi(bridge as any).kickMember(12345, 67890, false), + ).rejects.toThrow('群主无法被移出群聊'); + }); + it('kickMembers resolves each UID in parallel', async () => { const bridge = mockBridge(); vi.mocked(bridge.resolveUserUid) @@ -88,6 +105,20 @@ describe('apis/group-admin', () => { expect(env.body?.rejectAddRequest ?? 0).toBe(0); }); + it('kickMembers rejects the same command-level business error', async () => { + const bridge = mockBridge(); + bridge.sendRawPacket.mockResolvedValueOnce(packResponse( + Buffer.from( + '222008b960121be7bea4e4b8bbe697a0e6b395e8a2abe7a7bbe587bae7bea4e8818a', + 'hex', + ), + )); + + await expect( + new GroupAdminApi(bridge as any).kickMembers(12345, [67890], false), + ).rejects.toThrow('群主无法被移出群聊'); + }); + it('leave sends 0x1097_1, emits a self group_member_leave, and forgets the group (#133)', async () => { const bridge = mockBridge(); await new GroupAdminApi(bridge as any).leave(12345); diff --git a/packages/proto-defs/src/oidb-actions/base.ts b/packages/proto-defs/src/oidb-actions/base.ts index dc5d7bae..d74a84eb 100644 --- a/packages/proto-defs/src/oidb-actions/base.ts +++ b/packages/proto-defs/src/oidb-actions/base.ts @@ -62,6 +62,14 @@ export interface OidbKickMember { rejectAddRequest?: pb<4, bool>; reason?: pb<5, string>; } +// 0x8A0_1 response body. Cross-checked against Lagrange.Core's +// OidbSvcTrpcTcp0x8A0_1Response and the current QQ kick result contract: +// transport/envelope success does not imply the member was removed; a +// command-level refusal is returned in errorMsg. +export interface OidbKickMemberResponse { + groupUin?: pb<1, uint_32>; + errorMsg?: pb<2, string>; +} export interface OidbLeaveGroup { groupUin?: pb<1, uint_32>; } diff --git a/packages/protocol/src/oidb-services/group-admin/kick-member.ts b/packages/protocol/src/oidb-services/group-admin/kick-member.ts index 3c23365f..42e038ea 100644 --- a/packages/protocol/src/oidb-services/group-admin/kick-member.ts +++ b/packages/protocol/src/oidb-services/group-admin/kick-member.ts @@ -3,8 +3,11 @@ // namespace because proton needs distinct types per call site. import { protobuf_decode, protobuf_encode } from '@snowluma/proton'; -import type { OidbBase, OidbEmpty } from '@snowluma/proto-defs/oidb'; -import type { OidbKickMember } from '@snowluma/proto-defs/oidb-actions/base'; +import type { OidbBase } from '@snowluma/proto-defs/oidb'; +import type { + OidbKickMember, + OidbKickMemberResponse, +} from '@snowluma/proto-defs/oidb-actions/base'; import { invokeOidb, type OidbSender } from '../../oidb-service'; import type { BridgeContext } from '../../bridge-context'; @@ -29,13 +32,17 @@ export namespace KickMember { reason: p.reason ?? '', }); - export const deserialize = (_ctx: Deps, _: OidbEmpty): void => {}; + export const deserialize = (_ctx: Deps, body: OidbKickMemberResponse): void => { + if (body.errorMsg) { + throw new Error(`kick member failed: ${body.errorMsg}`); + } + }; export const encode = (env: OidbBase): Uint8Array => protobuf_encode>(env); - export const decode = (bytes: Uint8Array): OidbBase => - protobuf_decode>(bytes); + export const decode = (bytes: Uint8Array): OidbBase => + protobuf_decode>(bytes); export const invoke = (deps: Deps, params: Params): Promise => invokeOidb(deps, KickMember, params); diff --git a/packages/protocol/src/oidb-services/group-admin/kick-members.ts b/packages/protocol/src/oidb-services/group-admin/kick-members.ts index e46cfa5b..5ca2ab9c 100644 --- a/packages/protocol/src/oidb-services/group-admin/kick-members.ts +++ b/packages/protocol/src/oidb-services/group-admin/kick-members.ts @@ -3,8 +3,11 @@ // namespace so proton can monomorphize independently. import { protobuf_decode, protobuf_encode } from '@snowluma/proton'; -import type { OidbBase, OidbEmpty } from '@snowluma/proto-defs/oidb'; -import type { Oidb0x8a0Req } from '@snowluma/proto-defs/oidb-actions/base'; +import type { OidbBase } from '@snowluma/proto-defs/oidb'; +import type { + Oidb0x8a0Req, + OidbKickMemberResponse, +} from '@snowluma/proto-defs/oidb-actions/base'; import { invokeOidb, type OidbSender } from '../../oidb-service'; import type { BridgeContext } from '../../bridge-context'; @@ -28,13 +31,17 @@ export namespace KickMembers { field12: 0, }); - export const deserialize = (_ctx: Deps, _: OidbEmpty): void => {}; + export const deserialize = (_ctx: Deps, body: OidbKickMemberResponse): void => { + if (body.errorMsg) { + throw new Error(`kick members failed: ${body.errorMsg}`); + } + }; export const encode = (env: OidbBase): Uint8Array => protobuf_encode>(env); - export const decode = (bytes: Uint8Array): OidbBase => - protobuf_decode>(bytes); + export const decode = (bytes: Uint8Array): OidbBase => + protobuf_decode>(bytes); export const invoke = (deps: Deps, params: Params): Promise => invokeOidb(deps, KickMembers, params);