meeting-knowledge-smoke-test.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. const assert = require('assert/strict');
  2. const fs = require('fs');
  3. const os = require('os');
  4. const path = require('path');
  5. const { createMeetingKnowledgeService } = require('../mcp/src/dashboard/meeting-knowledge-service');
  6. const { OpenAICompatibleClient, AnthropicCompatibleClient } = require('../mcp/src/core/agent-runtime');
  7. async function testEmptyToolCompatibility() {
  8. const originalFetch = global.fetch;
  9. const requests = [];
  10. global.fetch = async (url, options) => {
  11. requests.push({ url: String(url), body: JSON.parse(options.body) });
  12. if (String(url).includes('/chat/completions')) {
  13. return { ok: true, json: async () => ({ choices: [{ message: { content: '{}' } }] }) };
  14. }
  15. return { ok: true, json: async () => ({ content: [{ type: 'text', text: '{}' }] }) };
  16. };
  17. try {
  18. await new OpenAICompatibleClient({ apiKey: 'test-only', baseUrl: 'https://model.test', model: 'test' }).complete([{ role: 'user', content: 'test' }], []);
  19. await new AnthropicCompatibleClient({ apiKey: 'test-only', baseUrl: 'https://model.test', model: 'test' }).complete([{ role: 'user', content: 'test' }], []);
  20. assert.equal(requests.length, 2);
  21. assert.equal('tools' in requests[0].body, false);
  22. assert.equal('tool_choice' in requests[0].body, false);
  23. assert.equal('tools' in requests[1].body, false);
  24. } finally {
  25. global.fetch = originalFetch;
  26. }
  27. }
  28. async function main() {
  29. await testEmptyToolCompatibility();
  30. const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'qiwei-meeting-knowledge-'));
  31. const liveStore = path.join(tempRoot, 'live');
  32. let round = 1;
  33. const officialStatus = async () => ({
  34. installed: true,
  35. valid: true,
  36. authorized: true,
  37. ready: true,
  38. installedVersion: '0.1.9',
  39. });
  40. const rpc = payload => ({ status: 'ok', data: { response: { jsonrpc: '2.0', id: 1, result: { content: [{ type: 'text', text: JSON.stringify(payload) }] } } } });
  41. const officialCall = async ({ method, args }) => {
  42. if (method === 'list_user_meetings') {
  43. assert.match(args.begin_datetime, /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/);
  44. assert.match(args.end_datetime, /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/);
  45. return rpc({ meetingid_list: [round === 1 ? 'meeting-live-1' : 'meeting-live-2'] });
  46. }
  47. assert.equal(method, 'get_meeting_info');
  48. const suffix = args.meetingid.endsWith('1') ? '一' : '二';
  49. return rpc({
  50. meetingid: args.meetingid,
  51. title: `客户项目推进会${suffix}`,
  52. meeting_start_datetime: '2026-07-16 14:00',
  53. meeting_duration: 3600,
  54. description: '王刚负责在 7 月 20 日前整理客户演示清单。',
  55. location: '线上会议室',
  56. status: 3,
  57. meeting_type: 0,
  58. password: 'must-not-be-saved',
  59. host_key: 'host-secret-value',
  60. attendees: {
  61. member: [{ userid: 'wanggang', status: 1, phone_number: '13800000000' }],
  62. tmp_external_user: [],
  63. },
  64. settings: { enable_waiting_room: true, credentials: 'private-value' },
  65. });
  66. };
  67. const analyzer = async () => ({
  68. status: 'completed',
  69. summary: '会议资料明确要求王刚整理客户演示清单。',
  70. topics: ['客户演示准备'],
  71. decisions: [],
  72. actionItems: [{
  73. title: '整理客户演示清单',
  74. owner: '王刚',
  75. dueDate: '2026-07-20',
  76. priority: 'high',
  77. evidence: '会议描述明确写明负责人和截止日期。',
  78. confidence: 0.98,
  79. }],
  80. suggestedActions: ['人工确认演示清单范围'],
  81. risks: ['缺少会议转写,无法验证其他讨论内容'],
  82. knowledgeTags: ['客户演示', '项目推进'],
  83. analysisBasis: '仅基于企业微信官方会议元数据与会议描述。',
  84. requiresReview: true,
  85. analyzedAt: new Date().toISOString(),
  86. error: null,
  87. });
  88. try {
  89. const service = createMeetingKnowledgeService({
  90. storeDir: liveStore,
  91. officialStatus,
  92. officialCall,
  93. meetingCapability: async () => ({ available: true, reason: 'available', message: '会议权限可用' }),
  94. analyzer,
  95. initCommand: 'node qiwei-official-cli.js init',
  96. });
  97. const firstSync = await service.sync();
  98. assert.equal(firstSync.status, 'ok');
  99. assert.equal(firstSync.summary.live, true);
  100. assert.equal(firstSync.summary.syncedCount, 1);
  101. assert.equal(firstSync.data.meetings[0].sourceKind, 'official-cli-live');
  102. const recordPath = firstSync.data.meetings[0].files.json;
  103. const markdownPath = firstSync.data.meetings[0].files.markdown;
  104. assert.ok(fs.existsSync(recordPath));
  105. assert.ok(fs.existsSync(markdownPath));
  106. const persisted = fs.readFileSync(recordPath, 'utf8');
  107. assert.doesNotMatch(persisted, /must-not-be-saved|host-secret-value|13800000000|private-value/);
  108. assert.doesNotMatch(persisted, /"password"|"host_key"|"phone_number"|"credentials"/);
  109. const analyzed = await service.analyze('meeting-live-1');
  110. assert.equal(analyzed.status, 'ok');
  111. assert.equal(analyzed.data.meeting.analysis.actionItems[0].owner, '王刚');
  112. assert.match(fs.readFileSync(markdownPath, 'utf8'), /整理客户演示清单/);
  113. round = 2;
  114. const secondSync = await service.sync();
  115. assert.equal(secondSync.summary.syncedCount, 1);
  116. assert.equal(secondSync.summary.meetingCount, 2);
  117. assert.equal(secondSync.data.meetings.find(item => item.id === 'meeting-live-1').analysis.status, 'completed');
  118. const hub = await service.hub();
  119. assert.equal(hub.status, 'ok');
  120. assert.equal(hub.summary.meetingCount, 2);
  121. assert.equal(hub.summary.actionItemCount, 1);
  122. const unauthorizedStore = path.join(tempRoot, 'unauthorized');
  123. const unauthorized = createMeetingKnowledgeService({
  124. storeDir: unauthorizedStore,
  125. officialStatus: async () => ({ installed: true, valid: true, authorized: false, ready: false }),
  126. officialCall: async () => { throw new Error('未授权时不应调用官方会议接口'); },
  127. initCommand: 'node qiwei-official-cli.js init',
  128. });
  129. const blocked = await unauthorized.sync();
  130. assert.equal(blocked.status, 'error');
  131. assert.equal(blocked.summary.needsInitialization, true);
  132. assert.equal(fs.existsSync(path.join(unauthorizedStore, 'index.json')), false);
  133. const unauthorizedHub = await unauthorized.hub();
  134. assert.equal(unauthorizedHub.data.meetings.length, 0);
  135. assert.equal(unauthorizedHub.data.cli.ready, false);
  136. assert.match(unauthorizedHub.data.cli.initCommand, /init/);
  137. let blockedOfficialCalls = 0;
  138. const policyBlocked = createMeetingKnowledgeService({
  139. storeDir: path.join(tempRoot, 'policy-blocked'),
  140. officialStatus,
  141. officialCall: async () => { blockedOfficialCalls += 1; throw new Error('企业策略受限时不应继续请求会议列表'); },
  142. meetingCapability: async () => ({ available: false, reason: 'enterprise-policy', message: '当前企业未开放会议 CLI' }),
  143. });
  144. const blockedHub = await policyBlocked.hub();
  145. assert.equal(blockedHub.summary.cliReady, true);
  146. assert.equal(blockedHub.summary.meetingReady, false);
  147. assert.equal(blockedHub.data.cli.capabilityReason, 'enterprise-policy');
  148. const policySync = await policyBlocked.sync();
  149. assert.equal(policySync.status, 'error');
  150. assert.equal(policySync.summary.needsMeetingCapability, true);
  151. assert.equal(blockedOfficialCalls, 0);
  152. process.stdout.write(`${JSON.stringify({
  153. status: 'ok',
  154. liveSyncContract: true,
  155. accumulatedMeetings: hub.summary.meetingCount,
  156. sensitiveFieldsRemoved: true,
  157. aiActionItemsPersisted: true,
  158. unauthorizedPathUsesNoMock: true,
  159. emptyToolModelCallsSupported: true,
  160. enterprisePolicyIsExplicit: true,
  161. jsonRpcEnvelopeSupported: true,
  162. }, null, 2)}\n`);
  163. } finally {
  164. fs.rmSync(tempRoot, { recursive: true, force: true });
  165. }
  166. }
  167. main().catch(error => {
  168. process.stderr.write(`${error.stack || error.message}\n`);
  169. process.exitCode = 1;
  170. });