feat: support image prompts across IM channels

This commit is contained in:
xmanrui 2026-08-19 02:56:14 +08:00
parent 99a877c640
commit 65c842c045
40 changed files with 3806 additions and 274 deletions

View file

@ -207,6 +207,127 @@ test('WhatsApp normalizes direct and explicitly mentioned group messages', () =>
}, ACCOUNT_JID), null);
});
test('WhatsApp exposes image media through a bounded Baileys download stream', async () => {
const png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const calls = [];
const controller = new AbortController();
const group = normalizeWhatsappMessage({
key: {
remoteJid: '120363000000000000@g.us',
participant: '16505550999@s.whatsapp.net',
id: 'group-image-1',
fromMe: false,
},
message: {
imageMessage: {
mimetype: 'image/png',
caption: '看看这张图',
fileLength: { toString: () => String(png.length) },
url: 'https://mmg.whatsapp.net/image',
contextInfo: { mentionedJid: [ACCOUNT_JID] },
},
},
}, ACCOUNT_JID, {
download: async (raw, type, options) => {
calls.push({ raw, type, options });
return {
async *[Symbol.asyncIterator]() { yield png; },
};
},
});
assert.equal(group.addressed, true);
assert.equal(group.content, '看看这张图');
assert.equal(group.images.length, 1);
assert.equal(group.images[0].size, png.length);
assert.deepEqual(await group.images[0].load({ signal: controller.signal, maxBytes: 100 }), png);
assert.equal(calls[0].type, 'stream');
assert.equal(calls[0].options.options.signal instanceof AbortSignal, true);
const downloadStarted = Promise.withResolvers();
const lateStream = Promise.withResolvers();
let lateStreamDestroyed = false;
const cancelled = normalizeWhatsappMessage({
key: { remoteJid: '16505550999@s.whatsapp.net', id: 'cancelled-1', fromMe: false },
message: {
imageMessage: { mimetype: 'image/png', url: 'https://mmg.whatsapp.net/cancelled' },
},
}, ACCOUNT_JID, {
download: async () => {
downloadStarted.resolve();
return lateStream.promise;
},
});
const cancelledController = new AbortController();
const cancelledLoad = cancelled.images[0].load({
signal: cancelledController.signal,
maxBytes: 100,
});
await downloadStarted.promise;
cancelledController.abort(new DOMException('Stopped', 'AbortError'));
await assert.rejects(cancelledLoad, { name: 'AbortError' });
lateStream.resolve({ destroy() { lateStreamDestroyed = true; } });
await new Promise((resolve) => setImmediate(resolve));
assert.equal(lateStreamDestroyed, true);
const document = normalizeWhatsappMessage({
key: { remoteJid: '16505550999@s.whatsapp.net', id: 'document-image-1', fromMe: false },
message: {
documentMessage: {
mimetype: 'image/webp', fileName: 'diagram.webp', fileLength: 2_000,
url: 'https://mmg.whatsapp.net/document',
},
},
}, ACCOUNT_JID);
assert.equal(document.images[0].name, 'diagram.webp');
assert.equal(document.images[0].mediaType, 'image/webp');
for (const [index, wrapper] of [
'viewOnceMessage',
'viewOnceMessageV2',
'viewOnceMessageV2Extension',
].entries()) {
const wrappedMessage = {
[wrapper]: {
message: {
imageMessage: {
mimetype: 'image/jpeg', url: `https://mmg.whatsapp.net/view-once-${index}`,
},
},
},
};
const viewOnce = normalizeWhatsappMessage({
key: {
remoteJid: '16505550999@s.whatsapp.net',
id: `view-once-${index}`,
fromMe: false,
},
message: index === 1
? { ephemeralMessage: { message: wrappedMessage } }
: wrappedMessage,
}, ACCOUNT_JID);
assert.deepEqual(viewOnce.images, []);
}
const oversized = normalizeWhatsappMessage({
key: { remoteJid: '16505550999@s.whatsapp.net', id: 'oversized-1', fromMe: false },
message: {
imageMessage: { mimetype: 'image/jpeg', url: 'https://mmg.whatsapp.net/oversized' },
},
}, ACCOUNT_JID, {
download: async () => ({
async *[Symbol.asyncIterator]() {
yield Buffer.alloc(4);
yield Buffer.alloc(4);
},
destroy() {},
}),
});
await assert.rejects(() => oversized.images[0].load({ maxBytes: 5 }), (error) => {
assert.equal(error.code, 'image-too-large');
return true;
});
});
test('WhatsApp runtime connects a linked device and replies through Harness', async () => {
let callbacks;
const calls = [];