fix(whatsapp): respond to linked-account group messages

This commit is contained in:
xmanrui 2026-08-25 01:12:55 +08:00
parent 9657107a21
commit 2cc3a42d45
11 changed files with 150 additions and 34 deletions

View file

@ -119,11 +119,13 @@ test('plain Slack, Telegram, Discord, and WhatsApp receipts retain every split m
let whatsappId = 0;
const remembered = [];
const reserved = [];
const whatsapp = new WhatsappBotClient({
sendPresenceUpdate: async () => {},
sendMessage: async () => ({ key: { id: `whatsapp-${++whatsappId}` } }),
}, {
remember: (messageId) => remembered.push(messageId),
reserve: (messageId) => reserved.push(messageId),
});
const whatsappReceipt = await textReceipt({
key: 'whatsapp',
@ -133,6 +135,9 @@ test('plain Slack, Telegram, Discord, and WhatsApp receipts retain every split m
});
assert.deepEqual(whatsappReceipt.providerMessageIds, ['whatsapp-1', 'whatsapp-2']);
assert.deepEqual(remembered, ['whatsapp-1', 'whatsapp-2']);
assert.equal(reserved.length, 2);
assert.equal(reserved.every((messageId) => /^[0-9A-F]{20}$/.test(messageId)), true);
assert.notEqual(reserved[0], reserved[1]);
});
test('Slack, Telegram, and Discord stream receipts retain the initial and remainder ids', async () => {

View file

@ -75,6 +75,7 @@ test('WhatsApp account card uses the unified compact channel layout', () => {
assert.match(markup, /仅自己模式(默认)/);
assert.match(markup, /指定联系人模式/);
assert.match(markup, /开放响应模式/);
assert.match(markup, /已绑定账号自己发出的群聊消息/);
assert.match(markup, /role="status"[^>]*>测试消息已发送/);
});

View file

@ -322,7 +322,7 @@ test('WhatsApp media downloader supplies Baileys reupload context', async () =>
assert.deepEqual(reuploaded, [{ key: { id: 'expired-media' } }]);
});
test('WhatsApp normalizes direct and explicitly mentioned group messages', () => {
test('WhatsApp normalizes direct, linked-account, and explicitly mentioned group messages', () => {
const direct = normalizeWhatsappMessage({
key: { remoteJid: '16505550999@s.whatsapp.net', id: 'direct-1', fromMe: false },
message: { conversation: 'hello' },
@ -358,6 +358,20 @@ test('WhatsApp normalizes direct and explicitly mentioned group messages', () =>
}, ACCOUNT_JID);
assert.equal(selfChat.selfChat, true);
assert.equal(selfChat.addressed, true);
const linkedAccountGroup = normalizeWhatsappMessage({
key: {
remoteJid: '120363000000000001@g.us',
id: 'owner-group-1',
fromMe: true,
},
message: { conversation: 'message from linked account in a group' },
}, ACCOUNT_JID);
assert.equal(linkedAccountGroup.kind, 'group');
assert.equal(linkedAccountGroup.senderId, ACCOUNT_JID);
assert.equal(linkedAccountGroup.addressed, true);
assert.equal(linkedAccountGroup.selfChat, false);
assert.equal(normalizeWhatsappMessage({
key: { remoteJid: '16505550999@s.whatsapp.net', id: 'outbound-1', fromMe: true },
message: { conversation: 'ordinary outbound message' },
@ -387,10 +401,19 @@ test('WhatsApp access modes allow self-chat, selected contacts, or the existing
key: { remoteJid: ACCOUNT_JID, id: 'access-self', fromMe: true },
message: { conversation: 'hello' },
}, ACCOUNT_JID);
const linkedAccountGroup = normalizeWhatsappMessage({
key: {
remoteJid: '120363000000000001@g.us',
id: 'access-owner-group',
fromMe: true,
},
message: { conversation: 'hello from the linked account' },
}, ACCOUNT_JID);
assert.equal(whatsappInboundAllowed(selfChat), true);
assert.equal(whatsappInboundAllowed(direct), false);
assert.equal(whatsappInboundAllowed(group), false);
assert.equal(whatsappInboundAllowed(linkedAccountGroup), false);
assert.equal(whatsappInboundAllowed(direct, {
accessMode: WHATSAPP_ACCESS_MODES.privateAllowlist,
allowedNumbers: new Set(['16505550999']),
@ -402,6 +425,9 @@ test('WhatsApp access modes allow self-chat, selected contacts, or the existing
assert.equal(whatsappInboundAllowed(group, {
accessMode: WHATSAPP_ACCESS_MODES.open,
}), true);
assert.equal(whatsappInboundAllowed(linkedAccountGroup, {
accessMode: WHATSAPP_ACCESS_MODES.open,
}), true);
});
test('WhatsApp keeps native and document images as images and exposes ordinary documents as files', async () => {
@ -623,6 +649,64 @@ test('WhatsApp runtime filters messages before the bridge and applies policy upd
await runtime.stop();
});
test('WhatsApp open mode answers linked-account group messages without processing reply echoes', async (t) => {
const groupJid = '120363000000000001@g.us';
let callbacks;
let replyEchoTask;
let askCount = 0;
const sent = [];
const socket = {
sendPresenceUpdate: async () => {},
readMessages: async () => {},
sendMessage: async (jid, content, options = {}) => {
sent.push({ jid, content, options });
replyEchoTask = callbacks.onMessage({
key: { remoteJid: groupJid, id: options.messageId, fromMe: true },
message: { conversation: content.text },
});
return { key: { id: options.messageId } };
},
};
const runtime = new WhatsappRuntime({
config: linkedConfig({ accessMode: WHATSAPP_ACCESS_MODES.open }),
authDir: '/tmp/test-whatsapp-linked-account-group',
harness: {
ensureRunning: async () => {},
sessionExists: async () => true,
ask: async () => {
askCount += 1;
return 'Harness group answer';
},
},
state: artifactState('session-linked-account-group'),
createSession: async (options) => {
callbacks = options;
return {
socket,
ready: Promise.resolve({ accountJid: ACCOUNT_JID, name: 'Harness WhatsApp' }),
close: async () => {},
logout: async () => {},
};
},
});
t.after(() => runtime.stop());
await runtime.start();
const inbound = {
key: { remoteJid: groupJid, id: 'linked-account-group-1', fromMe: true },
message: { conversation: 'hello from my group' },
};
await callbacks.onMessage(inbound);
await replyEchoTask;
assert.equal(askCount, 1);
assert.equal(sent.length, 1);
assert.equal(sent[0].jid, groupJid);
assert.equal(sent[0].content.text, 'Harness group answer');
assert.equal(sent[0].options.quoted, inbound);
assert.match(sent[0].options.messageId, /^[0-9A-F]{20}$/);
});
test('WhatsApp runtime sends result files with native metadata, quote, stable id, and upload timeout', async (t) => {
const { artifact, deliveryKey } = await committedArtifact(t, {
suffix: 'native-file',