| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135 |
- #!/usr/bin/env node
- const assert = require('assert');
- const fs = require('fs');
- const os = require('os');
- const path = require('path');
- const { GroupOperationsStore } = require('../mcp/src/core/group-operations-store');
- const { __testing: groupOpsTesting } = require('../mcp/src/tools/qiwei-group-operations-run');
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'qiwei-group-ops-'));
- const store = new GroupOperationsStore(path.join(tempDir, 'test.db'));
- try {
- const account = 'account-a';
- const otherAccount = 'account-b';
- const directoryAccount = 'directory-account';
- assert.equal(groupOpsTesting.syncCustomerGroupsToOperations(store, directoryAccount, [
- { roomId: 'confirmed-room', roomName: 'Confirmed room', reviewStatus: 'CONFIRMED' },
- { roomId: 'auto-room', roomName: 'Auto room', reviewStatus: 'AUTO_CONFIRMED' },
- { roomId: 'suggested-room', roomName: 'Suggested room', reviewStatus: 'SUGGESTED' },
- ]), 2);
- assert.deepEqual(store.listGroups(directoryAccount).map(group => group.room_id).sort(), ['auto-room', 'confirmed-room']);
- assert.equal(groupOpsTesting.syncCustomerGroupsToOperations(store, directoryAccount, [
- { roomId: 'confirmed-room', roomName: 'Confirmed room', reviewStatus: 'CONFIRMED' },
- { roomId: 'auto-room', roomName: 'Auto room', reviewStatus: 'AUTO_CONFIRMED' },
- ]), 0, 'directory reconciliation must be idempotent');
- const playbook = store.createPlaybook(account, {
- name: '新客群服务 SOP',
- groupType: 'new_customer',
- content: { cadence: [{ time: '09:30', type: 'welcome', objective: '欢迎新客户', template: '欢迎加入 {{room_name}}' }] }
- }, 'test');
- assert.equal(playbook.status, 'draft');
- const published = store.publishPlaybook(account, playbook.id, 1, 'test');
- assert.equal(published.status, 'published');
- store.upsertGroup(account, { roomId: 'room-1', roomName: '客户服务群', storeId: '门店A', groupType: 'new_customer', playbookId: playbook.id }, 'test');
- store.upsertGroup(account, { roomId: 'room-2', roomName: '会员服务群', storeId: '门店B', groupType: 'new_customer', playbookId: playbook.id }, 'test');
- assert.equal(store.listGroups(account).length, 2);
- assert.equal(store.listGroups(otherAccount).length, 0, '账号数据必须隔离');
- const version2 = store.addPlaybookVersion(account, playbook.id, { cadence: [{ time: '10:00', type: 'welcome', objective: '新版欢迎', template: '新版欢迎 {{room_name}}' }] }, '升级欢迎话术', 'test');
- assert.equal(version2.version, 2);
- const stillPublished = store.getPlaybook(account, playbook.id);
- assert.equal(stillPublished.status, 'published', '创建草稿版本不能停用当前已发布 SOP');
- assert.equal(stillPublished.version, 1, '创建草稿版本后默认读取仍应是当前已发布版本');
- const planBeforeV2Publish = store.generatePlan(account, { roomId: 'room-1', planDate: '2026-07-17' }, 'test');
- assert.equal(planBeforeV2Publish.items[0].draft_content, '欢迎加入 客户服务群', '未发布草稿不能进入运营计划');
- assert.equal(planBeforeV2Publish.playbook_version_id, stillPublished.current_version_id, '计划审计版本必须与实际内容一致');
- const impact = store.previewPlaybookPublish(account, playbook.id, 2);
- assert.equal(impact.affectedGroupCount, 2);
- assert.equal(impact.changes.length > 0, true);
- store.publishPlaybook(account, playbook.id, 2, 'test');
- store.publishPlaybook(account, playbook.id, 1, 'test');
- const plan = store.generatePlan(account, { roomId: 'room-1', planDate: '2026-07-18' }, 'test');
- assert.equal(plan.items.length, 1);
- assert.match(plan.items[0].draft_content, /客户服务群/);
- const duplicate = store.generatePlan(account, { roomId: 'room-1', planDate: '2026-07-18' }, 'test');
- assert.equal(duplicate.id, plan.id, '同群同日计划必须幂等');
- const approved = store.updatePlanItem(account, plan.items[0].id, 'approve', {}, 'test');
- assert.equal(approved.status, 'approved');
- store.updatePlanItem(account, plan.items[0].id, 'mark_sent', { externalMessageId: 'manual-1' }, 'test');
- const batch = store.batchGeneratePlans(account, { planDate: '2026-07-19', storeId: '门店B' }, 'test');
- assert.equal(batch.total, 1);
- assert.equal(batch.success, 1);
- const automationPlan = store.getPlan(batch.results[0].planId);
- const automationItem = store.updatePlanItem(account, automationPlan.items[0].id, 'approve', {}, 'test');
- const policy = store.createAutomationPolicy(account, { policy: { enabled: true, mode: 'dry_run', allowedGroupIds: ['room-2'], allowedTypes: [automationItem.type], windows: [{ days: [1], start: '09:00', end: '18:00' }], dailyLimitPerGroup: 1, weeklyLimitPerGroup: 7, consecutiveFailureLimit: 3, grayPercent: 100, requireNoOpenHighRisk: true } }, 'test');
- assert.equal(policy.status, 'draft');
- store.activateAutomationPolicy(account, policy.id, 'test');
- const automationAt = new Date('2026-07-20T10:00:00+08:00');
- assert.equal(store.evaluateAutomation(account, store.getPlanItemWithContext(account, automationItem.id), automationAt).allowed, false, '激活后必须保持暂停');
- store.updateAutomationState(account, 'resume', 'test');
- const decision = store.evaluateAutomation(account, store.getPlanItemWithContext(account, automationItem.id), automationAt);
- assert.equal(decision.allowed, true);
- assert.equal(store.createSendAttempt(account, store.getPlanItemWithContext(account, automationItem.id), decision, 'dry_run').status, 'dry_run');
- for (let index = 0; index < 3; index++) {
- const attempt = store.createSendAttempt(account, store.getPlanItemWithContext(account, automationItem.id), decision, 'live');
- store.finishSendAttempt(account, attempt.id, { success: false, error: `模拟失败 ${index + 1}` });
- }
- assert.equal(store.getAutomationState(account).breaker_open, 1, '连续失败必须熔断');
- assert.equal(store.evaluateAutomation(account, store.getPlanItemWithContext(account, automationItem.id), automationAt).reasons.includes('熔断器已打开'), true);
- store.updateAutomationState(account, 'reset_breaker', 'test');
- assert.equal(store.getAutomationState(account).breaker_open, 0);
- const ambiguousAttempt = store.createSendAttempt(account, store.getPlanItemWithContext(account, automationItem.id), decision, 'live');
- assert.equal(store.finishSendAttempt(account, ambiguousAttempt.id, { success: false, ambiguous: true, error: '发送回执超时' }).status, 'unknown');
- assert.equal(store.evaluateAutomation(account, store.getPlanItemWithContext(account, automationItem.id), automationAt).reasons.some(reason => reason.includes('结果待确认')), true, '回执未知时必须锁住重试');
- store.updateAutomationState(account, 'pause', 'test');
- const review = store.reviewMessages(account, {
- roomId: 'room-1', slaMinutes: 30, messages: [
- { messageId: 'm1', senderId: 'customer-1', senderRole: 'customer', content: '这个问题怎么退款?', sentAt: '2026-07-18T08:00:00.000Z' }
- ]
- }, 'test');
- assert.equal(review.findingCount, 2, '应识别退款风险和超时未回应');
- const unrelated = store.reviewMessages(account, {
- roomId: 'room-2', slaMinutes: 30, messages: [
- { messageId: 'q-unrelated', senderId: 'customer-2', senderRole: 'customer', content: '退款什么时候到账?', sentAt: '2026-07-18T08:00:00.000Z' },
- { messageId: 'a-unrelated', senderId: 'staff-2', senderRole: 'staff', content: '今天天气不错', sentAt: '2026-07-18T08:01:00.000Z' }
- ]
- }, 'test');
- assert.equal(unrelated.findings.some(item => item.type === 'unanswered_question'), true, '无关员工消息不能算有效回应');
- const answered = store.reviewMessages(account, {
- roomId: 'room-2', slaMinutes: 30, messages: [
- { messageId: 'a-related', senderId: 'staff-2', senderRole: 'staff', content: '退款已提交,预计今天下午原路到账。', sentAt: '2026-07-18T08:02:00.000Z' }
- ]
- }, 'test');
- assert.equal(answered.findings.filter(item => item.type === 'unanswered_question').length, 0, '有效回复后本次质检不应新增未回应结论');
- const configured = store.setQualitySettings(account, { slaMinutes: 45, riskTerms: ['投诉', '退款'], forbiddenClaims: ['保证'] }, 'test');
- assert.equal(configured.sla_minutes, 45);
- const overview = store.overview(account, '2026-07-18');
- assert.equal(overview.metrics.executionRate, 100);
- assert.equal(overview.metrics.highRisk >= 1, true);
- assert.equal(Array.isArray(overview.scores), true);
- assert.equal(overview.scores.length, 2);
- assert.equal(overview.scores.every(item => item.coverage >= 0 && item.coverage <= 100), true);
- store.updateFinding(account, overview.findings[0].id, { status: 'false_positive', resolution: '测试误报' }, 'test');
- assert.equal(store.feedbackSummary(account).falsePositives, 1);
- const task = store.createTask(account, { roomId: 'room-1', findingId: overview.findings[1].id, title: '跟进客户退款问题', owner: '测试负责人', dueAt: '2026-07-20T18:00:00+08:00', priority: 'high' }, 'test');
- assert.equal(task.status, 'open');
- assert.equal(store.updateTask(account, task.id, { status: 'done' }, 'test').status, 'done');
- assert.equal(store.storeBreakdown(account, '2026-07-18').length, 2);
- store.reviewMessages(account, {
- roomId: 'room-1',
- messages: [
- { messageId: 'recent-activity', senderId: 'customer-1', senderRole: 'customer', content: '收到,谢谢', sentAt: new Date().toISOString() }
- ]
- }, 'test');
- assert.equal(store.groupInsights(account, 'room-1')[0].activity.currentMessages >= 1, true);
- assert.equal(store.excellentScripts(account).length, 1);
- assert.equal(store.overview(otherAccount, '2026-07-18').metrics.groups, 0);
- console.log('group-operations smoke: ok');
- } finally {
- store.close();
- fs.rmSync(tempDir, { recursive: true, force: true });
- }
|