'use strict'; const VAGUE_ACKNOWLEDGEMENT_PATTERN = /^(?:好的?|行|可以|没问题|明白|收到|嗯)[,,。!!\s]*(?:[^。!?]{0,14})?[。!]?$/; const REPLY_TOPIC_MATCHERS = [ { topic: 'price', inbound: /什么价|多少钱|怎么收费|报价|费用|单价|总价/, reply: /价|费|万|元|预算|报价|核价/ }, { topic: 'appointment', inbound: /预约|约时间|哪天方便|几点|到店|上门/, reply: /约|时间|上午|下午|哪天|档期|方便/ }, { topic: 'availability', inbound: /还有没有|有没有名额|有货吗|档期还有/, reply: /有|没有|名额|库存|档期|名额/ }, { topic: 'policy', inbound: /资质|资格|政策|合同|退款|发票/, reply: /资质|资格|政策|合同|退款|发票|人工|核验/ }, { topic: 'complaint', inbound: /投诉|不处理|太差|骗人/, reply: /抱歉|记录|主管|处理|跟进|接管/ }, { topic: 'timeline', inbound: /什么时候|多久|周期|交付|上线/, reply: /时间|周期|天|周|月|交付|确认/ }, { topic: 'spec', inbound: /规格|型号|版本|套餐|配置/, reply: /规格|型号|版本|套餐|配置|方案/ }, ]; const TABOO_PHRASES = Object.freeze([ '我先帮您梳理一下需求', '请您耐心等待', '我们一直致力于提供优质服务', '为了更好地为您服务', '系统提示', '当前缺少判断依据', '已降级为人工核验', ]); function clauseSimilarity(a = '', b = '') { const strip = value => String(value || '').replace(/[\s,。!?、;:""''()()]/g, ''); const x = strip(a); const y = strip(b); if (x.length < 6 || y.length < 6) return 0; const grams = text => { const map = new Map(); for (let index = 0; index < text.length - 1; index += 1) { const gram = text.slice(index, index + 2); map.set(gram, (map.get(gram) || 0) + 1); } return map; }; const gx = grams(x); const gy = grams(y); let overlap = 0; for (const [gram, count] of gx) overlap += Math.min(count, gy.get(gram) || 0); const total = [...gx.values()].reduce((sum, count) => sum + count, 0) + [...gy.values()].reduce((sum, count) => sum + count, 0); return total ? (2 * overlap) / total : 0; } function deduplicateResponseClauses(reply = '') { const clauses = String(reply || '').match(/[^。!?\n]+[。!?]?/g) || []; const kept = []; for (const clause of clauses) { if (kept.some(previous => clauseSimilarity(previous, clause) >= 0.5)) continue; kept.push(clause); } return kept.join(''); } function normalizeWeChatReply(reply = '') { const normalized = deduplicateResponseClauses(String(reply || '')) .trim() .replace(/,/g, ',') .replace(/\?/g, '?') .replace(/!/g, '!'); if (!normalized) return ''; const sentences = normalized.match(/[^。!?\n]+[。!?]?/g) || [normalized]; const kept = []; let questions = 0; for (const sentence of sentences) { const question = /?/.test(sentence); if (question && questions >= 1) continue; if (question) questions += 1; kept.push(sentence.trim()); if (kept.length >= 3) break; } return kept.join(''); } function pickReplyVariant(seed = '', variants = []) { if (!variants.length) return ''; let value = 2166136261; for (const char of String(seed || '')) { value ^= char.charCodeAt(0); value = Math.imul(value, 16777619); } return variants[(value >>> 0) % variants.length]; } function replyStyleViolations(reply = '', inbound = '', _generationStrategy = {}, styleContext = {}) { const text = String(reply || '').trim(); const ask = String(inbound || ''); if (!text) return []; const violations = []; if (/(?:帮|替|给)(?:您|你).{0,6}(?:查|问|找|确认|核实|核对|安排|转达)|(?:确认|核实)(?:好|完|后).{0,8}(?:回|发|告诉|答复)|稍等.{0,8}(?:我|回)/.test(text)) { violations.push('unverified_commitment'); } if (/(?:肯定|保证|一定|绝对)(?:能|会|有|可以|给|没问题|不会)|包(?:您|你)|百分百|准没错/.test(text)) { violations.push('absolute_guarantee'); } if (styleContext.hasAppointmentEvidence !== true) { const acceptsNamedDate = (/(?:今天|明天|后天|周[一二三四五六日天末])/.test(text) && /(?:我(?:有空|可以|能)|可以安排|能安排|没问题|都行|随时)/.test(text)) || /(?:今天|明天|后天|周[一二三四五六日天末])(?:是|就)?(?:可以|行|没问题|方便)/.test(text); const promisesArrangement = /(?:预约|到店|上门).{0,6}(?:没问题|都行|随时|好说)/.test(text) || /(?:按|依|照)(?:您|你)(?:的)?(?:时间|档期|节奏)(?:来)?(?:安排|定|走|排)/.test(text) || /(?:帮|替|给)(?:您|你).{0,8}(?:约|安排|排).{0,6}(?:时间|档期|上门|到店)/.test(text); if (acceptsNamedDate || promisesArrangement) violations.push('schedule_confirmation'); } if (/(?:尽快|抓紧|赶紧|趁早|要快)|(?:再不|错过).{0,10}(?:就没|可惜)/.test(text)) { violations.push('customer_pressure'); } if (/(?:资料|系统|数据库|后台|记录)(?:里|中)?.{0,8}(?:没有|查不到|看不出|未收录)|缺少.{0,12}判断依据|没有新的核验结果|当前(?:还)?缺少|需要(?:顾问|专员|同事).{0,6}确认/.test(text)) { violations.push('system_speak'); } const customerText = [ask, ...(styleContext.messages || []) .filter(message => (message?.role === 'user' || message?.direction === 'inbound')) .map(message => String(message?.content || ''))].join('\n'); const numbers = text.match(/\d+(?:\.\d+)?/g) || []; const quotesUnsourcedNumber = numbers.some(number => !customerText.includes(number)) && /(?:%|%|万|元|年|折)/.test(text) && styleContext.hasCatalogEvidence !== true; if (quotesUnsourcedNumber) violations.push('unsourced_number'); if (/税/.test(ask) && /(?:免税|不用交税|全免)|(?:税率|税额).{0,8}\d/.test(text)) { violations.push('tax_conclusion'); } if (/投诉|退款|赔偿|合同纠纷|隐私泄露/.test(ask) && !/(?:人工|主管|负责人).{0,10}(?:复核|核验|确认|接管)/.test(text)) { violations.push('risk_handoff_missing'); } if (VAGUE_ACKNOWLEDGEMENT_PATTERN.test(text)) violations.push('vague_acknowledgement'); for (const matcher of REPLY_TOPIC_MATCHERS) { if (matcher.inbound.test(ask) && !matcher.reply.test(text)) { violations.push(`off_topic:${matcher.topic}`); break; } } if (text.length <= 24 && !/[??]/.test(text) && /(?:要看|得看|取决于|看具体|看情况)/.test(text) && !/(?:一般|通常|大多|因为|比如|差别|不一样)/.test(text)) { violations.push('deflection_only'); } if ((text.match(/[??]/g) || []).length >= 3) violations.push('question_overload'); const recentOutbound = (styleContext.messages || []) .filter(message => (message?.role === 'assistant' || message?.direction === 'outbound')) .slice(-4) .map(message => String(message?.content || '')) .filter(Boolean); if (recentOutbound.some(previous => clauseSimilarity(previous, text) >= 0.82)) { violations.push('repeat_previous'); } return violations; } function fallbackForViolations(violations = [], inbound = '', seed = '') { const codes = new Set(violations.map(item => String(item).split(':')[0])); const pick = (key, variants) => pickReplyVariant(`${seed}\n${key}`, variants); if (codes.has('risk_handoff_missing')) { return pick('handoff', [ '这件事需要主管复核,我先记下您的诉求,结论出来前先按未处理完成看待。', '投诉和责任判断要转主管跟进。您最希望先解决哪一项?', ]); } if (codes.has('schedule_confirmation')) { return pick('schedule', [ '具体时间还要再核对档期,我先记下您方便的时段。您更想上午还是下午?', '预约还没落到日历上,先不确定已经约好。您哪天更方便?', ]); } if (codes.has('absolute_guarantee') || codes.has('unsourced_number')) { return pick('boundary', [ '这一项我现在不能打包票,得按核验结果说。您最想先确认价格、时间还是范围?', '现有信息还不够下确定结论。您把最关键的一项条件再说清楚,我按这个往下核。', ]); } if (codes.has('unverified_commitment') || codes.has('system_speak')) { return pick('no-process', [ '我先按现有信息回答;还没核到的部分不先说满。您最想先确认哪一项?', '能确定的我直接说,拿不准的不替工具下结论。您更在意进度还是具体条件?', ]); } if (codes.has('vague_acknowledgement') || codes.has('deflection_only') || codes.has('off_topic')) { return pick('answer', [ '您这个问题我先直接说:当前还要结合具体条件确认。您把最关键的一项发我。', '我先回应您刚问的点。把具体对象或条件给我一个,我好对上再给下一步。', ]); } if (codes.has('repeat_previous') || codes.has('question_overload') || codes.has('customer_pressure')) { return pick('pace', [ '刚才那句说绕了。您按自己的节奏来,有明确问题我再补一句。', '不催您决定。您先看当前这条是否把问题说清,不清楚再问一个点就行。', ]); } return pick('generic', [ '我先按能确定的说,拿不准的不替您承诺。您最想先确认哪一项?', ]); } function enforceHumanReplyStyle(final = {}, inboundContent = '', generationStrategy = {}, styleContext = {}) { const inbound = String(inboundContent || ''); const original = String(final.reply || ''); if (!original.trim()) return final; const violations = replyStyleViolations(original, inbound, generationStrategy, styleContext); if (!violations.length) { const clean = normalizeWeChatReply(original.replace(/您们/g, '你们')); if (!clean.trim() || clean === original) return final; return { ...final, reply: clean, reason: `${String(final.reason || '')}${final.reason ? ';' : ''}服务端仅做微信节奏归一,未改写模型语气。`, }; } const seed = [ styleContext.variantKey, generationStrategy.stage, generationStrategy.scenario, inbound, original, ].filter(Boolean).join('\n'); return { ...final, reply: fallbackForViolations(violations, inbound, seed), reason: `${String(final.reason || '')}${final.reason ? ';' : ''}服务端因回复越线已改写:${violations.join('、')}。`, styleViolations: violations, }; } function enforceTabooFreeReply(final = {}) { let reply = String(final.reply || '').trim(); if (!reply) return final; const hits = []; for (const phrase of TABOO_PHRASES) { if (reply.includes(phrase)) { hits.push(phrase); reply = reply.split(phrase).join(''); } } if (!hits.length) return final; reply = reply .replace(/[。!?,、\s]+(?=[。!?,、])/g, '') .replace(/^(?:[,,、。;:]+|[了得的]?[,,、。;:])/, '') .replace(/\s{2,}/g, ' ') .trim(); if (!reply || !/[^\s,。!?;:、]/u.test(reply)) return final; return { ...final, reply, reason: `${String(final.reason || '')}${final.reason ? ';' : ''}服务端已删除聊天风格禁忌语:${hits.join('、')}。`, }; } function enforceSingleFinalReply(final = {}) { const text = String(final.reply || '').trim(); if (!text) return final; const markers = [ /(?:^|[。!?\n;]|[((【\[]\s*)(?:以下是|这是|上面是|我的最终回复是)?\s*(?:按(?:聊天|沟通)?风格处理(?:后|过)?的?(?:最终)?(?:回复|消息|版本)|检查后(?:生成的)?(?:的)?(?:最终)?(?:回复|消息)|最终(?:回复|消息|版本))[))】\]]?\s*[::]?\s*/g, /(?:^|[。!?\n;])\s*[((【\[]\s*(?:按(?:聊天|沟通)?风格处理(?:后|过)?|检查后|风格化(?:处理)?)\s*[))】\]]\s*[::]?\s*/g, ]; let cut = -1; for (const rx of markers) { let match; while ((match = rx.exec(text))) { const next = match.index + match[0].length; if (text.slice(next).trim() && next > cut) cut = next; } } if (cut < 0) return final; const finalPart = text.slice(cut).trim(); if (!finalPart) return final; return { ...final, reply: finalPart, reason: `${String(final.reason || '')}${final.reason ? ';' : ''}服务端检测到多版本回复,已按“最终版本”收敛为单条。`, }; } module.exports = { TABOO_PHRASES, clauseSimilarity, deduplicateResponseClauses, enforceHumanReplyStyle, enforceSingleFinalReply, enforceTabooFreeReply, normalizeWeChatReply, replyStyleViolations, };