fix(qq): respond to all mentioned group members

This commit is contained in:
xmanrui 2026-08-31 00:45:59 +08:00
parent 7f968ac1d8
commit 4d18cc4de0
4 changed files with 227 additions and 196 deletions

View file

@ -6,6 +6,11 @@ This file records the notable changes in each dsh-im release. Its format follows
## [Unreleased]
### Fixed / 修复
- QQ 扫码绑定的机器人现在会响应群内任意成员对机器人的 @ 消息,同时继续只接受扫码者的私聊;群聊仍不会响应未 @ 机器人的普通消息。
QQ bots connected by QR code now respond when any group member mentions the bot, while private chats remain restricted to the scanner. Ordinary group messages without a mention remain ignored.
## [4.1.0] - 2026-08-30
### Added / 新增

File diff suppressed because one or more lines are too long

View file

@ -109,6 +109,15 @@ function conversationKey(message) {
return `${message.kind}:${message.kind === 'group' ? message.groupOpenid : message.senderId}`;
}
function senderAllowed(message, ownerUserOpenid) {
// QR binding yields a C2C user_openid, while group events identify senders
// with a group-scoped member_openid. Treat group membership plus @mention as
// the access boundary, and keep the scanner restriction for private chats.
return message?.kind === 'group'
|| ownerUserOpenid === '*'
|| message?.senderId === ownerUserOpenid;
}
function safeText(message) {
return typeof message?.content === 'string' ? message.content.trim() : '';
}
@ -442,7 +451,7 @@ export class QqHarnessBridge {
}
const pending = this.#pendingInteractions.get(key);
const commandText = safeText(message);
const allowed = this.#ownerUserOpenid === '*' || sender === this.#ownerUserOpenid;
const allowed = senderAllowed(message, this.#ownerUserOpenid);
const addressed = message.kind !== 'group'
|| message.rawEventType === 'GROUP_AT_MESSAGE_CREATE';
const batchCommand = isBatchInputCommand(commandText);
@ -569,7 +578,7 @@ export class QqHarnessBridge {
alreadyRecorded = false,
batchSubmission = null,
} = {}) {
const allowed = this.#ownerUserOpenid === '*' || message.senderId === this.#ownerUserOpenid;
const allowed = senderAllowed(message, this.#ownerUserOpenid);
const addressed = message.kind !== 'group'
|| message.rawEventType === 'GROUP_AT_MESSAGE_CREATE';
const preparedMessage = allowed && addressed
@ -721,7 +730,7 @@ export class QqHarnessBridge {
await this.#state.markSeen(messageId);
messageRecorded = true;
};
if (this.#ownerUserOpenid !== '*' && sender !== this.#ownerUserOpenid) {
if (!senderAllowed(message, this.#ownerUserOpenid)) {
this.#status.messagesRejected += 1;
this.#status.lastRejectedAt = new Date().toISOString();
return;

View file

@ -789,6 +789,7 @@ test('QQ remembers any authorized private inbound as a connection-test target',
await bridge.accept(message({
kind: 'group',
rawEventType: 'GROUP_AT_MESSAGE_CREATE',
senderId: 'group-member-openid',
groupOpenid: 'group-1',
messageId: 'help-group',
content: '/help',
@ -1089,8 +1090,8 @@ test('QQ keeps a stopped turn terminal when its notice cannot be sent', async ()
assert.equal(fixture.seen.has('qq-stopped-stream-fallback'), true);
});
test('QQ bridge accepts only the scanner and requires an at-message event in groups', async () => {
let asks = 0;
test('QQ bridge keeps private chats scanner-only and accepts any mentioned group member', async () => {
const asks = [];
const state = {
hasSeen: () => false,
markSeen: async () => {},
@ -1102,15 +1103,31 @@ test('QQ bridge accepts only the scanner and requires an at-message event in gro
const bridge = new QqHarnessBridge({
bot: { sendText: async () => {} },
ownerUserOpenid: 'owner-openid',
harness: { sessionExists: async () => true, ask: async () => { asks += 1; return 'ok'; } },
harness: {
sessionExists: async () => true,
ask: async (sessionId, text) => { asks.push({ sessionId, text }); return 'ok'; },
},
state,
});
await bridge.accept(message({ messageId: 'other', senderId: 'other-openid' }));
await bridge.accept(message({
messageId: 'group', kind: 'group', groupOpenid: 'group-1', rawEventType: 'GROUP_MESSAGE_CREATE',
replyTarget: { scope: 'group', targetId: 'group-1', msgId: 'group' },
messageId: 'group-unmentioned',
kind: 'group',
senderId: 'other-member-openid',
groupOpenid: 'group-1',
rawEventType: 'GROUP_MESSAGE_CREATE',
replyTarget: { scope: 'group', targetId: 'group-1', msgId: 'group-unmentioned' },
}));
assert.equal(asks, 0);
await bridge.accept(message({
messageId: 'group-mentioned',
kind: 'group',
senderId: 'other-member-openid',
groupOpenid: 'group-1',
rawEventType: 'GROUP_AT_MESSAGE_CREATE',
content: '群成员的问题',
replyTarget: { scope: 'group', targetId: 'group-1', msgId: 'group-mentioned' },
}));
assert.deepEqual(asks, [{ sessionId: 'session', text: '群成员的问题' }]);
assert.equal(bridge.status.messagesRejected, 1);
});