| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182 |
- const assert = require('assert/strict');
- const fs = require('fs');
- const os = require('os');
- const path = require('path');
- const { createMeetingKnowledgeService } = require('../mcp/src/dashboard/meeting-knowledge-service');
- const { OpenAICompatibleClient, AnthropicCompatibleClient } = require('../mcp/src/core/agent-runtime');
- async function testEmptyToolCompatibility() {
- const originalFetch = global.fetch;
- const requests = [];
- global.fetch = async (url, options) => {
- requests.push({ url: String(url), body: JSON.parse(options.body) });
- if (String(url).includes('/chat/completions')) {
- return { ok: true, json: async () => ({ choices: [{ message: { content: '{}' } }] }) };
- }
- return { ok: true, json: async () => ({ content: [{ type: 'text', text: '{}' }] }) };
- };
- try {
- await new OpenAICompatibleClient({ apiKey: 'test-only', baseUrl: 'https://model.test', model: 'test' }).complete([{ role: 'user', content: 'test' }], []);
- await new AnthropicCompatibleClient({ apiKey: 'test-only', baseUrl: 'https://model.test', model: 'test' }).complete([{ role: 'user', content: 'test' }], []);
- assert.equal(requests.length, 2);
- assert.equal('tools' in requests[0].body, false);
- assert.equal('tool_choice' in requests[0].body, false);
- assert.equal('tools' in requests[1].body, false);
- } finally {
- global.fetch = originalFetch;
- }
- }
- async function main() {
- await testEmptyToolCompatibility();
- const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'qiwei-meeting-knowledge-'));
- const liveStore = path.join(tempRoot, 'live');
- let round = 1;
- const officialStatus = async () => ({
- installed: true,
- valid: true,
- authorized: true,
- ready: true,
- installedVersion: '0.1.9',
- });
- const rpc = payload => ({ status: 'ok', data: { response: { jsonrpc: '2.0', id: 1, result: { content: [{ type: 'text', text: JSON.stringify(payload) }] } } } });
- const officialCall = async ({ method, args }) => {
- if (method === 'list_user_meetings') {
- assert.match(args.begin_datetime, /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/);
- assert.match(args.end_datetime, /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/);
- return rpc({ meetingid_list: [round === 1 ? 'meeting-live-1' : 'meeting-live-2'] });
- }
- assert.equal(method, 'get_meeting_info');
- const suffix = args.meetingid.endsWith('1') ? '一' : '二';
- return rpc({
- meetingid: args.meetingid,
- title: `客户项目推进会${suffix}`,
- meeting_start_datetime: '2026-07-16 14:00',
- meeting_duration: 3600,
- description: '王刚负责在 7 月 20 日前整理客户演示清单。',
- location: '线上会议室',
- status: 3,
- meeting_type: 0,
- password: 'must-not-be-saved',
- host_key: 'host-secret-value',
- attendees: {
- member: [{ userid: 'wanggang', status: 1, phone_number: '13800000000' }],
- tmp_external_user: [],
- },
- settings: { enable_waiting_room: true, credentials: 'private-value' },
- });
- };
- const analyzer = async () => ({
- status: 'completed',
- summary: '会议资料明确要求王刚整理客户演示清单。',
- topics: ['客户演示准备'],
- decisions: [],
- actionItems: [{
- title: '整理客户演示清单',
- owner: '王刚',
- dueDate: '2026-07-20',
- priority: 'high',
- evidence: '会议描述明确写明负责人和截止日期。',
- confidence: 0.98,
- }],
- suggestedActions: ['人工确认演示清单范围'],
- risks: ['缺少会议转写,无法验证其他讨论内容'],
- knowledgeTags: ['客户演示', '项目推进'],
- analysisBasis: '仅基于企业微信官方会议元数据与会议描述。',
- requiresReview: true,
- analyzedAt: new Date().toISOString(),
- error: null,
- });
- try {
- const service = createMeetingKnowledgeService({
- storeDir: liveStore,
- officialStatus,
- officialCall,
- meetingCapability: async () => ({ available: true, reason: 'available', message: '会议权限可用' }),
- analyzer,
- initCommand: 'node qiwei-official-cli.js init',
- });
- const firstSync = await service.sync();
- assert.equal(firstSync.status, 'ok');
- assert.equal(firstSync.summary.live, true);
- assert.equal(firstSync.summary.syncedCount, 1);
- assert.equal(firstSync.data.meetings[0].sourceKind, 'official-cli-live');
- const recordPath = firstSync.data.meetings[0].files.json;
- const markdownPath = firstSync.data.meetings[0].files.markdown;
- assert.ok(fs.existsSync(recordPath));
- assert.ok(fs.existsSync(markdownPath));
- const persisted = fs.readFileSync(recordPath, 'utf8');
- assert.doesNotMatch(persisted, /must-not-be-saved|host-secret-value|13800000000|private-value/);
- assert.doesNotMatch(persisted, /"password"|"host_key"|"phone_number"|"credentials"/);
- const analyzed = await service.analyze('meeting-live-1');
- assert.equal(analyzed.status, 'ok');
- assert.equal(analyzed.data.meeting.analysis.actionItems[0].owner, '王刚');
- assert.match(fs.readFileSync(markdownPath, 'utf8'), /整理客户演示清单/);
- round = 2;
- const secondSync = await service.sync();
- assert.equal(secondSync.summary.syncedCount, 1);
- assert.equal(secondSync.summary.meetingCount, 2);
- assert.equal(secondSync.data.meetings.find(item => item.id === 'meeting-live-1').analysis.status, 'completed');
- const hub = await service.hub();
- assert.equal(hub.status, 'ok');
- assert.equal(hub.summary.meetingCount, 2);
- assert.equal(hub.summary.actionItemCount, 1);
- const unauthorizedStore = path.join(tempRoot, 'unauthorized');
- const unauthorized = createMeetingKnowledgeService({
- storeDir: unauthorizedStore,
- officialStatus: async () => ({ installed: true, valid: true, authorized: false, ready: false }),
- officialCall: async () => { throw new Error('未授权时不应调用官方会议接口'); },
- initCommand: 'node qiwei-official-cli.js init',
- });
- const blocked = await unauthorized.sync();
- assert.equal(blocked.status, 'error');
- assert.equal(blocked.summary.needsInitialization, true);
- assert.equal(fs.existsSync(path.join(unauthorizedStore, 'index.json')), false);
- const unauthorizedHub = await unauthorized.hub();
- assert.equal(unauthorizedHub.data.meetings.length, 0);
- assert.equal(unauthorizedHub.data.cli.ready, false);
- assert.match(unauthorizedHub.data.cli.initCommand, /init/);
- let blockedOfficialCalls = 0;
- const policyBlocked = createMeetingKnowledgeService({
- storeDir: path.join(tempRoot, 'policy-blocked'),
- officialStatus,
- officialCall: async () => { blockedOfficialCalls += 1; throw new Error('企业策略受限时不应继续请求会议列表'); },
- meetingCapability: async () => ({ available: false, reason: 'enterprise-policy', message: '当前企业未开放会议 CLI' }),
- });
- const blockedHub = await policyBlocked.hub();
- assert.equal(blockedHub.summary.cliReady, true);
- assert.equal(blockedHub.summary.meetingReady, false);
- assert.equal(blockedHub.data.cli.capabilityReason, 'enterprise-policy');
- const policySync = await policyBlocked.sync();
- assert.equal(policySync.status, 'error');
- assert.equal(policySync.summary.needsMeetingCapability, true);
- assert.equal(blockedOfficialCalls, 0);
- process.stdout.write(`${JSON.stringify({
- status: 'ok',
- liveSyncContract: true,
- accumulatedMeetings: hub.summary.meetingCount,
- sensitiveFieldsRemoved: true,
- aiActionItemsPersisted: true,
- unauthorizedPathUsesNoMock: true,
- emptyToolModelCallsSupported: true,
- enterprisePolicyIsExplicit: true,
- jsonRpcEnvelopeSupported: true,
- }, null, 2)}\n`);
- } finally {
- fs.rmSync(tempRoot, { recursive: true, force: true });
- }
- }
- main().catch(error => {
- process.stderr.write(`${error.stack || error.message}\n`);
- process.exitCode = 1;
- });
|