agent-inbound-media-smoke-test.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. 'use strict';
  2. const assert = require('assert');
  3. const fs = require('fs');
  4. const os = require('os');
  5. const path = require('path');
  6. const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'qiwei-inbound-media-'));
  7. process.env.QIWEI_OUTPUTS_DIR = path.join(tempRoot, 'outputs');
  8. const { AgentWorkbenchDb } = require('../mcp/src/core/agent-workbench-db');
  9. const { AgentWorkbenchService } = require('../mcp/src/core/agent-workbench-service');
  10. const { evaluatePolledMessage } = require('../mcp/src/core/agent-poller-policy');
  11. const { InboundMediaService, mediaMessageDescriptor, sniffMedia } = require('../mcp/src/core/inbound-media');
  12. async function main() {
  13. const imageMessage = {
  14. msgType: 14,
  15. senderId: 'contact-1',
  16. timestamp: 1785920000,
  17. msgData: { fileId: 'file-1', fileAesKey: 'key-1', fileSize: 68, width: 1, height: 1 },
  18. };
  19. const imageCandidate = evaluatePolledMessage(imageMessage, { allowedSenders: ['contact-1'] });
  20. assert.equal(imageCandidate.eligible, true);
  21. assert.equal(imageCandidate.content, '[图片]');
  22. assert.equal(imageCandidate.media.kind, 'image');
  23. assert.equal(imageCandidate.generateReply, false);
  24. const voiceCandidate = evaluatePolledMessage({
  25. msgType: 16,
  26. senderId: 'contact-1',
  27. timestamp: 1785920001,
  28. msgData: { fileId: 'voice-1', fileAesKey: 'key-2', voiceTime: 6 },
  29. }, { allowedSenders: ['contact-1'] });
  30. assert.equal(voiceCandidate.eligible, true);
  31. assert.equal(voiceCandidate.content, '[语音]');
  32. assert.equal(voiceCandidate.generateReply, false);
  33. const analyzedVoice = mediaMessageDescriptor({
  34. msgType: 16,
  35. msgData: { transcript: '我明天下午有时间' },
  36. });
  37. assert.equal(analyzedVoice.mediaOnly, false);
  38. assert.equal(analyzedVoice.content, '[语音] 我明天下午有时间');
  39. const groupCandidate = evaluatePolledMessage({ ...imageMessage, fromRoomId: 'room-1' }, { allowedSenders: ['contact-1'] });
  40. assert.equal(groupCandidate.eligible, false);
  41. assert.equal(groupCandidate.reason, 'group_message');
  42. const db = new AgentWorkbenchDb(path.join(tempRoot, 'workbench.db'));
  43. let agentCalls = 0;
  44. const service = new AgentWorkbenchService({
  45. db,
  46. agent: { run: async () => { agentCalls += 1; throw new Error('agent must not run'); } },
  47. qiwei: {},
  48. config: { qiwei: { allowedSenders: ['contact-1'] }, agent: {} },
  49. });
  50. const stored = await service.ingestInbound({
  51. externalId: 'image-message-1',
  52. contactId: 'contact-1',
  53. contactName: '测试联系人',
  54. content: imageCandidate.content,
  55. timestamp: new Date(1785920000 * 1000).toISOString(),
  56. raw: imageMessage,
  57. contentType: imageCandidate.media.contentType,
  58. payload: imageCandidate.media.payload,
  59. }, { generateReply: false });
  60. assert.equal(stored.status, 'media_received');
  61. assert.equal(agentCalls, 0);
  62. const second = await service.ingestInbound({
  63. externalId: 'image-message-2',
  64. contactId: 'contact-1',
  65. contactName: '测试联系人',
  66. content: imageCandidate.content,
  67. timestamp: new Date((1785920000 + 1) * 1000).toISOString(),
  68. raw: { ...imageMessage, msgServerId: 'image-message-2' },
  69. contentType: imageCandidate.media.contentType,
  70. payload: imageCandidate.media.payload,
  71. }, { generateReply: false });
  72. assert.equal(second.status, 'media_received');
  73. const rows = db.listMessages(stored.conversation.id);
  74. assert.equal(rows.length, 2);
  75. assert.equal(rows[0].content_type, 'image');
  76. assert.equal(JSON.parse(rows[0].raw_json).msgData.fileId, 'file-1');
  77. if (typeof service.stopBackgroundWorkers === 'function') service.stopBackgroundWorkers();
  78. db.close();
  79. const png = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64');
  80. assert.equal(sniffMedia(png, 'application/octet-stream').contentType, 'image/png');
  81. let downloadCalls = 0;
  82. const resolver = new InboundMediaService({
  83. qiwei: {
  84. call: async (method, params, options) => {
  85. assert.equal(method, '/cloud/wxWorkDownload');
  86. assert.equal(params.fileType, 1);
  87. assert.equal(options.sanitizeResponse, false);
  88. return { cloudUrl: 'https://media.example.test/image.png' };
  89. },
  90. },
  91. fetchImpl: async url => {
  92. downloadCalls += 1;
  93. assert.equal(url, 'https://media.example.test/image.png');
  94. return new Response(png, { status: 200, headers: { 'content-type': 'image/png' } });
  95. },
  96. });
  97. const resolved = await resolver.resolve('message-1', imageMessage);
  98. assert.equal(downloadCalls, 1);
  99. assert.equal(resolved.contentType, 'image/png');
  100. assert.equal(fs.readFileSync(resolved.filePath).equals(png), true);
  101. const dashboardApp = fs.readFileSync(path.join(__dirname, '..', 'mcp', 'src', 'dashboard', 'app.js'), 'utf8');
  102. const dashboardServer = fs.readFileSync(path.join(__dirname, '..', 'mcp', 'src', 'dashboard', 'server.js'), 'utf8');
  103. const agentService = fs.readFileSync(path.join(__dirname, '..', 'mcp', 'src', 'dashboard', 'agent-service.js'), 'utf8');
  104. const workbenchService = fs.readFileSync(path.join(__dirname, '..', 'mcp', 'src', 'core', 'agent-workbench-service.js'), 'utf8');
  105. assert.match(dashboardApp, /data-media-retry/);
  106. assert.match(dashboardApp, /\/api\/agent\/messages\/\$\{encodeURIComponent\(message\.id\)\}\/media/);
  107. assert.match(dashboardServer, /\/api\\\/agent\\\/messages\\\/\(\[\^\/\]\+\)\\\/media/);
  108. assert.match(agentService, /function publicMessageRaw/);
  109. assert.match(workbenchService, /media_received_without_auto_reply/);
  110. fs.rmSync(tempRoot, { recursive: true, force: true });
  111. console.log('agent inbound media smoke: passed');
  112. }
  113. main().catch(error => {
  114. try { fs.rmSync(tempRoot, { recursive: true, force: true }); } catch {}
  115. console.error(error);
  116. process.exit(1);
  117. });