Files
crm-ai/bridge/src/escalation.js
T

73 lines
3.8 KiB
JavaScript

'use strict';
// 三级升级策略(旧 bridge escalation/manager 的 LibreDesk 版):
// Level 0 正常;Level 1 追加“是否转人工”;Level 2 私聊备注 + 分配人工。
function createEscalation({ client, handoffUserId = 1 }) {
function shouldEscalate(state, intent, kbConfidence, consecutiveFails, kbHitCount = 0) {
const processed = Number(state && state.messagesProcessed) || 0;
if (consecutiveFails >= 3) return { level: 2, reason: 'consecutive_fails' };
if (processed > 8 && !(state && state.opportunityId)) return { level: 2, reason: 'long_no_lead' };
if (intent === 'policy_faq' && kbConfidence === 'low' && kbHitCount === 0) return { level: 1, reason: 'low_kb_confidence' };
if (kbConfidence === 'none' && !['product_search', 'sales_lead', 'order_tracking', 'inventory_check'].includes(intent)) return { level: 1, reason: 'no_kb_match' };
if (processed > 5 && consecutiveFails >= 2) return { level: 1, reason: 'repeated_low_confidence' };
return { level: 0, reason: null };
}
function appendEscalationOption(reply, query) {
const text = String(reply || '').replace(/\s+$/, '');
const hints = {
zh: '您想转人工客服吗?',
en: 'Would you like to speak to a human agent?',
de: 'Möchten Sie mit einem Mitarbeiter sprechen?',
fr: 'Souhaitez-vous parler à un conseiller ?',
it: 'Vuole parlare con un operatore?',
nl: 'Wilt u met een medewerker spreken?',
es: '¿Desea hablar con un agente humano?',
tr: 'Bir insan temsilcisiyle görüşmek ister misiniz?',
};
// 只输出一种语言:按用户输入判断语言,识别不到时用英文;不再拼英文兜底。
let lang = null;
if (/[\u4e00-\u9fff]/.test(query)) lang = 'zh';
else if (/[ğışç]/i.test(query) || /\b(merhaba|istiyorum|konuşmak|temsilci|fiyat|ürün|sipariş)\b/i.test(query)) lang = 'tr';
else if (/[äöüß]/i.test(query)) lang = 'de';
else if (/[ñ¿¡]/i.test(query) || /\b(hola|quiero|hablar|agente|puedo|gracias|precio|pedido)\b/i.test(query)) lang = 'es';
else if (/\b(vorrei|parlare|operatore|desidera|posso|prezzo|ordine)\b/i.test(query)) lang = 'it';
else if (/[éèêëàâùûîïôç]/i.test(query) || /\b(bonjour|merci|conseiller|commande|livraison|prix|voudrais)\b/i.test(query)) lang = 'fr';
else if (/\b(hoe|voor|wilt|bent|geen|verzending|levering|bestelling)\b/i.test(query)) lang = 'nl';
const option = `\n\n${hints[lang] || hints.en}`;
return text + option;
}
async function escalateToHuman({ convUuid, trigger, history }) {
const lines = [`Escalation trigger: ${trigger || 'unknown'}`];
if (history && history.length) {
lines.push('Recent context:');
for (const m of history.slice(-4)) {
const role = m.role === 'user' ? 'Customer' : 'AI';
lines.push(` [${role}] ${String(m.content || '').slice(0, 150)}`);
}
}
try {
await client.sendPrivateNote(convUuid, lines.join('\n'));
await client.assignUser(convUuid, handoffUserId);
return true;
} catch (err) {
console.error(JSON.stringify({ ts: new Date().toISOString(), warn: `escalate failed: ${err.message}` }));
return false;
}
}
function applyEmotionStrategy(emotion) {
switch (emotion) {
case 'angry': return { skipAI: false, escalate: true, maxToolTurns: 2, tone: 'empathetic' };
case 'frustrated': return { skipAI: false, escalate: false, maxToolTurns: 3, tone: 'patient' };
case 'urgent': return { skipAI: false, escalate: false, maxToolTurns: 3, tone: 'direct' };
default: return { skipAI: false, escalate: false, maxToolTurns: 5, tone: 'normal' };
}
}
return { shouldEscalate, appendEscalationOption, escalateToHuman, applyEmotionStrategy };
}
module.exports = { createEscalation };