fix: aggregate WeChat multi-image batches
This commit is contained in:
@@ -4029,6 +4029,150 @@ test('wechat mp serializes image and follow-up text and reattaches recent image'
|
||||
assert.equal(submitCalls[1].userMessage.metadata.msgType, 'text');
|
||||
});
|
||||
|
||||
test('wechat mp groups consecutive images for one follow-up and clears the consumed batch', async () => {
|
||||
const token = 'token';
|
||||
const timestamp = '1710000000';
|
||||
const nonce = 'nonce';
|
||||
const testUserId = 'test-user-multi-image-followup';
|
||||
const submitCalls = [];
|
||||
let eventCall = 0;
|
||||
let releaseFirst = null;
|
||||
const service = createBoundWechatService({
|
||||
token,
|
||||
config: {
|
||||
mediaAnalysisGrayUsers: [testUserId],
|
||||
},
|
||||
userAuth: {
|
||||
async findWechatUserByOpenid() {
|
||||
return { userId: testUserId, status: 'active', nickname: '唐' };
|
||||
},
|
||||
},
|
||||
sessionApiFetch: async (_sessionId, pathname) => {
|
||||
if (pathname === '/sessions/session-1/events') {
|
||||
eventCall += 1;
|
||||
if (eventCall === 1) {
|
||||
return new Response(
|
||||
new ReadableStream({
|
||||
start(controller) {
|
||||
releaseFirst = () => {
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(
|
||||
'data: {"type":"Message","message":{"id":"assistant-first","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"第一张处理完成。"}]}}\n\n' +
|
||||
'data: {"type":"Finish"}\n\n',
|
||||
),
|
||||
);
|
||||
controller.close();
|
||||
};
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||||
);
|
||||
}
|
||||
return new Response(
|
||||
[
|
||||
'data: {"type":"Message","message":{"id":"assistant-ok","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"处理完成。"}]}}\n\n',
|
||||
'data: {"type":"Finish"}\n\n',
|
||||
].join(''),
|
||||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||||
);
|
||||
}
|
||||
if (pathname === '/agent/harness_remember' || pathname === '/agent/harness_bootstrap') {
|
||||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
throw new Error(`unexpected api path: ${pathname}`);
|
||||
},
|
||||
submitSessionReply: async (input) => {
|
||||
submitCalls.push(input);
|
||||
return { ok: true };
|
||||
},
|
||||
wechatFetch: async (url) => {
|
||||
if (String(url).includes('/cgi-bin/stable_token')) {
|
||||
return new Response(JSON.stringify({ access_token: 'access-1', expires_in: 7200 }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
if (String(url).includes('/cgi-bin/media/get')) {
|
||||
return new Response(Buffer.from([0x89, 0x50, 0x4e, 0x47]), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'image/png' },
|
||||
});
|
||||
}
|
||||
if (String(url).includes('/cgi-bin/message/custom/send')) {
|
||||
return new Response(JSON.stringify({ errcode: 0, errmsg: 'ok' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected wechat url: ${url}`);
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const firstImageResult = await service.handleInboundMessage(
|
||||
inboundXml({
|
||||
msgType: 'image',
|
||||
content: '',
|
||||
extraFields: {
|
||||
MsgId: '10011',
|
||||
MediaId: 'media-first',
|
||||
PicUrl: 'https://wx.example.com/media-first.png',
|
||||
},
|
||||
}),
|
||||
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
|
||||
);
|
||||
while (submitCalls.length === 0) await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
const secondImageResult = await service.handleInboundMessage(
|
||||
inboundXml({
|
||||
msgType: 'image',
|
||||
content: '',
|
||||
extraFields: {
|
||||
MsgId: '10012',
|
||||
MediaId: 'media-second',
|
||||
PicUrl: 'https://wx.example.com/media-second.png',
|
||||
},
|
||||
}),
|
||||
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
|
||||
);
|
||||
|
||||
const followupResult = await service.handleInboundMessage(
|
||||
inboundXml({
|
||||
content: '请结合刚才两张图片分析主题',
|
||||
extraFields: { MsgId: '10013' },
|
||||
}),
|
||||
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.equal(submitCalls.length, 1);
|
||||
|
||||
releaseFirst();
|
||||
await firstImageResult.task;
|
||||
await secondImageResult.task;
|
||||
await followupResult.task;
|
||||
|
||||
const laterResult = await service.handleInboundMessage(
|
||||
inboundXml({
|
||||
content: '请再次分析刚才图片',
|
||||
extraFields: { MsgId: '10014' },
|
||||
}),
|
||||
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
|
||||
);
|
||||
await laterResult.task;
|
||||
} finally {
|
||||
fs.rmSync(path.join(process.cwd(), 'MindSpace', testUserId), { recursive: true, force: true });
|
||||
}
|
||||
|
||||
assert.equal(submitCalls.length, 4);
|
||||
assert.equal(submitCalls[0].userMessage.metadata.imageUrls.length, 1);
|
||||
assert.equal(submitCalls[1].userMessage.metadata.imageUrls.length, 1);
|
||||
assert.equal(submitCalls[2].userMessage.metadata.imageUrls.length, 2);
|
||||
assert.match(submitCalls[2].userMessage.metadata.imageUrls[0], /media-first/);
|
||||
assert.match(submitCalls[2].userMessage.metadata.imageUrls[1], /media-second/);
|
||||
assert.equal(submitCalls[2].userMessage.metadata.msgType, 'text');
|
||||
assert.equal(submitCalls[3].userMessage.metadata.imageUrls, undefined);
|
||||
});
|
||||
|
||||
test('wechat mp service persists Word and Excel files in user public area and reuses H5 attachment metadata', async () => {
|
||||
const token = 'token';
|
||||
const timestamp = '1710000000';
|
||||
|
||||
Reference in New Issue
Block a user