feat: add WeChat media analysis adapters
This commit is contained in:
@@ -3686,6 +3686,7 @@ test('wechat mp service persists image and routes image url into agent prompt',
|
||||
const nonce = 'nonce';
|
||||
const testUserId = 'test-user-image';
|
||||
const prompts = [];
|
||||
const metadataCalls = [];
|
||||
const detailCalls = [];
|
||||
const service = createWechatMpService({
|
||||
config: {
|
||||
@@ -3747,6 +3748,7 @@ test('wechat mp service persists image and routes image url into agent prompt',
|
||||
if (pathname === '/sessions/session-1/reply') {
|
||||
const body = JSON.parse(init.body);
|
||||
prompts.push(body.user_message.content[0].text);
|
||||
metadataCalls.push(body.user_message.metadata);
|
||||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
if (pathname === '/agent/harness_remember' || pathname === '/agent/harness_bootstrap') {
|
||||
@@ -3809,10 +3811,170 @@ test('wechat mp service persists image and routes image url into agent prompt',
|
||||
prompts[0],
|
||||
/\[图片1\]: https:\/\/example\.com\/MindSpace\/test-user-image\/public\/wechat-mp\//,
|
||||
);
|
||||
assert.equal(metadataCalls.length, 1);
|
||||
assert.equal(metadataCalls[0].source, 'wechat_mp');
|
||||
assert.equal(metadataCalls[0].msgType, 'image');
|
||||
assert.equal(metadataCalls[0].imageUrls.length, 1);
|
||||
assert.match(metadataCalls[0].imageUrls[0], /\/public\/wechat-mp\//);
|
||||
assert.equal(detailCalls.length, 1);
|
||||
assert.match(detailCalls[0].mediaPublicUrl, /\/wechat-mp\//);
|
||||
});
|
||||
|
||||
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';
|
||||
const nonce = 'nonce';
|
||||
const testUserId = 'test-user-wechat-file';
|
||||
const userMessages = [];
|
||||
const service = createBoundWechatService({
|
||||
token,
|
||||
config: {
|
||||
maxFileBytes: 1024 * 1024,
|
||||
acceptFile: true,
|
||||
},
|
||||
userAuth: {
|
||||
async findWechatUserByOpenid() {
|
||||
return { userId: testUserId, status: 'active', nickname: '毕升' };
|
||||
},
|
||||
},
|
||||
sessionApiFetch: async (_sessionId, pathname, init = {}) => {
|
||||
if (pathname === '/sessions/session-1/events') {
|
||||
return new Response(
|
||||
[
|
||||
'data: {"type":"Message","request_id":"req-file","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"文件已解析。"}]}}\n\n',
|
||||
'data: {"type":"Finish","request_id":"req-file","token_state":{"inputTokens":1,"outputTokens":2}}\n\n',
|
||||
].join(''),
|
||||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||||
);
|
||||
}
|
||||
if (pathname === '/sessions/session-1/reply') {
|
||||
const body = JSON.parse(init.body);
|
||||
userMessages.push(body.user_message);
|
||||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
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}`);
|
||||
},
|
||||
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('fake-docx-content'), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/octet-stream' },
|
||||
});
|
||||
}
|
||||
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}`);
|
||||
},
|
||||
});
|
||||
|
||||
const originalRandomUuid = crypto.randomUUID;
|
||||
crypto.randomUUID = () => 'req-file';
|
||||
try {
|
||||
for (const [index, filename] of ['季度分析.docx', '销售数据.xlsx'].entries()) {
|
||||
const result = await service.handleInboundMessage(
|
||||
inboundXml({
|
||||
msgType: 'file',
|
||||
content: '',
|
||||
extraFields: {
|
||||
MediaId: `file-media-${index + 1}`,
|
||||
FileName: filename,
|
||||
FileSize: 17,
|
||||
},
|
||||
}),
|
||||
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
|
||||
);
|
||||
assert.equal(result.status, 200);
|
||||
await result.task;
|
||||
const attachment = userMessages.at(-1)?.metadata?.fileAttachments?.[0];
|
||||
const publicFilename = decodeURIComponent(new URL(attachment.downloadUrl).pathname.split('/').at(-1));
|
||||
assert.equal(
|
||||
fs.existsSync(
|
||||
path.join(process.cwd(), 'MindSpace', testUserId, 'public', 'wechat-mp', publicFilename),
|
||||
),
|
||||
true,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
crypto.randomUUID = originalRandomUuid;
|
||||
fs.rmSync(path.join(process.cwd(), 'MindSpace', testUserId), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
|
||||
assert.equal(userMessages.length, 2);
|
||||
assert.match(userMessages[0].content[0].text, /【微信服务号文件消息】/);
|
||||
assert.match(userMessages[0].content[0].text, /\[文件1: 季度分析\.docx\]:/);
|
||||
assert.equal(userMessages[0].metadata.msgType, 'file');
|
||||
assert.equal(userMessages[0].metadata.fileAttachments.length, 1);
|
||||
assert.equal(userMessages[0].metadata.fileAttachments[0].filename, '季度分析.docx');
|
||||
assert.equal(
|
||||
userMessages[0].metadata.fileAttachments[0].mimeType,
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
);
|
||||
assert.match(userMessages[0].metadata.fileAttachments[0].downloadUrl, /\/public\/wechat-mp\//);
|
||||
assert.match(userMessages[1].content[0].text, /\[文件1: 销售数据\.xlsx\]:/);
|
||||
assert.equal(userMessages[1].metadata.fileAttachments[0].filename, '销售数据.xlsx');
|
||||
assert.equal(
|
||||
userMessages[1].metadata.fileAttachments[0].mimeType,
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
);
|
||||
assert.match(userMessages[1].metadata.fileAttachments[0].downloadUrl, /\/public\/wechat-mp\//);
|
||||
});
|
||||
|
||||
test('wechat mp service rejects unsupported public file types without entering the agent flow', async () => {
|
||||
let mediaDownloads = 0;
|
||||
let sessionCalls = 0;
|
||||
const service = createBoundWechatService({
|
||||
config: { acceptFile: true },
|
||||
sessionApiFetch: async () => {
|
||||
sessionCalls += 1;
|
||||
throw new Error('session should not be called');
|
||||
},
|
||||
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')) mediaDownloads += 1;
|
||||
throw new Error(`unexpected wechat url: ${url}`);
|
||||
},
|
||||
});
|
||||
|
||||
const result = await service.handleInboundMessage(
|
||||
inboundXml({
|
||||
msgType: 'file',
|
||||
content: '',
|
||||
extraFields: { MediaId: 'file-media-2', FileName: 'payload.exe' },
|
||||
}),
|
||||
{
|
||||
timestamp: '1710000000',
|
||||
nonce: 'nonce',
|
||||
signature: signatureFor('token', '1710000000', 'nonce'),
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(result.status, 200);
|
||||
assert.match(result.body, /仅支持 Word/);
|
||||
assert.equal(mediaDownloads, 0);
|
||||
assert.equal(sessionCalls, 0);
|
||||
});
|
||||
|
||||
test('wechat mp service accepts full voice xml payload and routes recognition text into agent', async () => {
|
||||
const token = 'token';
|
||||
const timestamp = '1710000000';
|
||||
|
||||
Reference in New Issue
Block a user