const ToolExecutor = require('./toolExecutor'); const db = require('../db'); const { hasKnowledgeRouteKeyword } = require('./knowledgeKeywords'); function normalizeTextForSpeech(text) { return (text || '') .replace(/^#{1,6}\s*/gm, '') .replace(/\*\*([^*]*)\*\*/g, '$1') .replace(/__([^_]*)__/g, '$1') .replace(/\*([^*]+)\*/g, '$1') .replace(/_([^_]+)_/g, '$1') .replace(/~~([^~]*)~~/g, '$1') .replace(/`{1,3}[^`]*`{1,3}/g, '') .replace(/^[-*]{3,}\s*$/gm, '') .replace(/^>\s*/gm, '') .replace(/!\[[^\]]*\]\([^)]*\)/g, '') .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') .replace(/^[\s]*[-*+]\s+/gm, ' ') .replace(/^[\s]*\d+[.)]\s+/gm, ' ') .replace(/---\s*来源[::]/g, '来源:') .replace(/\r/g, ' ') .replace(/\n{2,}/g, '。') .replace(/\n/g, ' ') .replace(/。{2,}/g, '。') .replace(/([!?;,])\1+/g, '$1') .replace(/([。!?;,])\s*([。!?;,])/g, '$2') .replace(/\s+/g, ' ') .trim(); } function splitTextForSpeech(text, maxLen = 180) { const content = normalizeTextForSpeech(text); if (!content) return []; if (content.length <= maxLen) return [content]; const chunks = []; let remaining = content; while (remaining.length > maxLen) { const currentMaxLen = chunks.length === 0 ? Math.min(90, maxLen) : maxLen; let splitIndex = Math.max( remaining.lastIndexOf('。', currentMaxLen), remaining.lastIndexOf('!', currentMaxLen), remaining.lastIndexOf('?', currentMaxLen), remaining.lastIndexOf(';', currentMaxLen), remaining.lastIndexOf(',', currentMaxLen), remaining.lastIndexOf(',', currentMaxLen) ); if (splitIndex < Math.floor(currentMaxLen / 2)) { splitIndex = currentMaxLen; } else { splitIndex += 1; } chunks.push(remaining.slice(0, splitIndex).trim()); remaining = remaining.slice(splitIndex).trim(); } if (remaining) chunks.push(remaining); return chunks.filter(Boolean); } function estimateSpeechDurationMs(text) { const plainText = normalizeTextForSpeech(text).replace(/\s+/g, ''); const length = plainText.length; return Math.max(4000, Math.min(60000, length * 180)); } function normalizeKnowledgeAlias(text) { return String(text || '') .replace(/X{2}系统/gi, '一成系统') .replace(/一城系统|逸城系统|一程系统|易成系统|一诚系统|亦成系统|艺成系统|溢成系统|义成系统|毅成系统|怡成系统|以成系统|已成系统|亿成系统|忆成系统|益成系统|益生系统|易诚系统|义诚系统|忆诚系统|以诚系统|一声系统|亿生系统|易乘系统/g, '一成系统') .replace(/(? String(item?.content || '').trim()) .join('\n'); return hasKnowledgeKeyword(recentContextText); } function withHandoffSummary(session, context) { const summary = String(session?.handoffSummary || '').trim(); if (!summary || session?.handoffSummaryUsed) { return context; } return [ { role: 'assistant', content: `会话交接摘要:${summary}` }, ...(Array.isArray(context) ? context : []), ]; } function getRuleBasedDirectRouteDecision(userText) { const text = (userText || '').trim(); if (!text) return { route: 'chat', args: {} }; if (/(几点|几号|日期|星期|周几|现在时间|当前时间)/.test(text)) return { route: 'get_current_time', args: {} }; if (/(天气|气温|下雨|晴天|阴天|温度)/.test(text)) { return { route: 'query_weather', args: { city: text.replace(/.*?(北京|上海|广州|深圳|杭州|成都|重庆|武汉|西安|南京|苏州|天津|长沙|郑州|青岛|宁波|无锡)/, '$1') || '北京' } }; } if (/(订单|物流|快递|单号)/.test(text)) return { route: 'query_order', args: { order_id: text } }; if (/^[\d\s+\-*/().=%]+$/.test(text) || /(等于多少|帮我算|计算一下|算一下)/.test(text)) { return { route: 'calculate', args: { expression: text.replace(/(帮我算|计算一下|算一下|等于多少)/g, '').trim() || text } }; } if (/(传销|骗局|骗子|正规吗|合法吗|正不正规|合不合法|是不是传销|直销还是传销|层级分销|非法集资|拉人头|下线|发展下线|报单|人头费)/.test(text)) { return { route: 'search_knowledge', args: { query: text } }; } if (/(产品|公司.*产品|产品.*有哪些|有什么产品|产品介绍|产品列表|公司介绍|我们公司|你们公司|你们的产品|我们的产品|都有什么|有些什么)/.test(text)) { return { route: 'search_knowledge', args: { query: text } }; } if (/(培训|新人入门|新人.*起步|团队培训|会议组织|起步三关|成长上总裁|精品会议)/.test(text)) { return { route: 'search_knowledge', args: { query: text } }; } if (/(招商|代理|加盟|招募|事业机会|创业|代理商|招商稿|合作.*加盟|事业.*合作)/.test(text)) { return { route: 'search_knowledge', args: { query: text } }; } if (/(一成系统|Ai众享|AI众享|数字化工作室|盛咖学愿|邀约话术|文化解析|AI赋能|ai赋能|团队发展|一成AI|一成Ai|AI落地|ai落地|转观念)/.test(text)) { return { route: 'search_knowledge', args: { query: text } }; } if (/(地址|电话|联系方式|总部|分公司|公司.*实力|公司.*背景|PM公司|德国PM)/.test(text)) { return { route: 'search_knowledge', args: { query: text } }; } if (/(成分|功效|怎么吃|怎么服用|吃法|用法|服用方法|副作用|好转反应|排毒反应|整应反应|搭配|原料|配方)/.test(text)) { return { route: 'search_knowledge', args: { query: text } }; } if (/(能吃吗|可以吃吗|能喝吗|可以喝吗|能用吗|可以用吗|一起吃|同时吃|混着吃|搭配吃|跟药一起)/.test(text)) { return { route: 'search_knowledge', args: { query: text } }; } if (/(发痒|头晕|便秘|腹泻|拉肚子|恶心|呕吐|长痘|出疹子|红肿|上火|胃痛|抽筋|浮肿|失眠|出汗)/.test(text)) { return { route: 'search_knowledge', args: { query: text } }; } if (/(高血压|糖尿病|胆固醇|心脏病|肾病|肝病|痛风|贫血|甲亢|甲减|胃炎|肠炎|哮喘|湿疹|过敏|癌症|肿瘤|尿酸|结石)/.test(text)) { return { route: 'search_knowledge', args: { query: text } }; } if (/(孕妇|哺乳期|怀孕|孕期|儿童|小孩|老人|老年人|手术后|化疗|放疗|术后|月子)/.test(text)) { return { route: 'search_knowledge', args: { query: text } }; } if (/(多少钱|价格|贵不贵|性价比|划算|值得买|保质期|储存|哪里买|怎么买|多久见效|适合谁|适用人群)/.test(text)) { return { route: 'search_knowledge', args: { query: text } }; } if (/(营养素|营养品|保健品|保健食品|营养补充|细胞营养|NTC|暖炉原理|火炉原理|阿育吠陀)/.test(text)) { return { route: 'search_knowledge', args: { query: text } }; } if (/(科普|误区|认证|检测|检测报告|安全认证|GMP|Halal|临床试验)/.test(text)) { return { route: 'search_knowledge', args: { query: text } }; } if (/(免疫力|抗疲劳|排毒|减肥|瘦身|护肤|护发|脱发|掉发|抗氧化|抗衰老|美容|美白|关节|骨密度|胶原蛋白)/.test(text)) { return { route: 'search_knowledge', args: { query: text } }; } if (/(信用评级|行业排名|获奖|慈善|社会责任|不上市|汽车奖励|退休金|旅行|福利|发展历程|全球布局|各国地址|各国电话|多少个国家)/.test(text)) { return { route: 'search_knowledge', args: { query: text } }; } if (/(奖金制度|事业机会|创业|副业|兼职|投资多少|门槛|起步费|怎么赚钱|能赚钱吗|值得做吗)/.test(text)) { return { route: 'search_knowledge', args: { query: text } }; } if (/^(喂|你好|您好|嗨|哈喽|hello|hi|在吗|在不在|早上好|中午好|下午好|晚上好|早安|晚安|谢谢|感谢|再见|拜拜|嗯|哦|好的|对|是的|没有了|没事了|可以了|行|OK|ok)[,,!。??~~\s]*[啊呀吧呢哦嗯嘛哈的了]*[!。??~~]*$/.test(text)) { return { route: 'chat', args: {} }; } if (/^(喂[,,\s]*)?(你好|您好)[,,!。??\s]*(在吗|请问)?[!。??]*$/.test(text)) { return { route: 'chat', args: {} }; } return { route: 'chat', args: {} }; } function extractToolResultText(toolName, toolResult) { if (!toolResult) return ''; if (toolName === 'search_knowledge') { if (toolResult.errorType === 'timeout') { return '知识库查询超时了,请稍后重试,或换一种更具体的问法再试。'; } if (toolResult.errorType === 'not_configured') { return '知识库当前未配置完成,请先检查知识库配置。'; } if (toolResult.errorType === 'endpoint_not_configured') { return '知识库已配置但方舟LLM端点未就绪,暂时无法检索,请稍后再试。'; } if (toolResult.results && Array.isArray(toolResult.results)) { return toolResult.results.map((item) => item.content || JSON.stringify(item)).join('\n'); } if (typeof toolResult === 'string') return toolResult; if (toolResult.error) return toolResult.error; } if (toolName === 'query_weather' && !toolResult.error) return `${toolResult.city}今天${toolResult.weather},气温${toolResult.temp},湿度${toolResult.humidity},${toolResult.wind}。${toolResult.tips || ''}`.trim(); if (toolName === 'query_order' && !toolResult.error) return `订单${toolResult.order_id}当前状态是${toolResult.status},预计送达时间${toolResult.estimated_delivery},快递单号${toolResult.tracking_number}。`; if (toolName === 'get_current_time' && !toolResult.error) return `现在是${toolResult.datetime},${toolResult.weekday}。`; if (toolName === 'calculate' && !toolResult.error) return `${toolResult.expression} 的计算结果是 ${toolResult.formatted}。`; if (toolResult.error) return toolResult.error; return typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult); } async function resolveReply(sessionId, session, text) { const recentMessages = await db.getRecentMessages(sessionId, 20).catch(() => []); const scopedMessages = session?.handoffSummaryUsed ? recentMessages.filter((item) => !/^chat_/i.test(String(item?.source || ''))) : recentMessages; const baseContext = scopedMessages .filter((item) => item && (item.role === 'user' || item.role === 'assistant')) .map((item) => ({ role: item.role, content: item.content })); const context = withHandoffSummary(session, baseContext); const originalText = text.trim(); let routeDecision = getRuleBasedDirectRouteDecision(text.trim()); if (routeDecision.route === 'chat' && shouldForceKnowledgeRoute(text.trim(), context)) { routeDecision = { route: 'search_knowledge', args: { query: text.trim() } }; } let replyText = ''; let source = 'voice_bot'; let toolName = null; let responseMeta = { route: routeDecision.route, original_text: originalText, }; if (routeDecision.route === 'chat') { session.handoffSummaryUsed = true; return { delivery: 'upstream_chat', speechText: '', ragItems: [], source, toolName, routeDecision, responseMeta, }; } else { toolName = routeDecision.route; source = 'voice_tool'; const toolArgs = toolName === 'search_knowledge' ? { ...(routeDecision.args || {}), response_mode: 'answer', session_id: sessionId } : routeDecision.args; const toolResult = await ToolExecutor.execute(routeDecision.route, toolArgs, context); replyText = extractToolResultText(toolName, toolResult); responseMeta = { ...responseMeta, tool_name: toolName, tool_args: toolArgs || {}, source: toolResult?.source || null, original_query: toolResult?.original_query || routeDecision.args?.query || originalText, rewritten_query: toolResult?.rewritten_query || null, selected_dataset_ids: toolResult?.selected_dataset_ids || null, selected_kb_routes: toolResult?.selected_kb_routes || null, hit: typeof toolResult?.hit === 'boolean' ? toolResult.hit : null, reason: toolResult?.reason || null, error_type: toolResult?.errorType || null, latency_ms: toolResult?.latency_ms || null, }; const ragItems = toolName === 'search_knowledge' ? (toolResult?.hit && Array.isArray(toolResult?.results) ? toolResult.results .filter((item) => item && item.content) .map((item) => ({ title: item.title || '知识库结果', content: item.content, })) : []) : (!toolResult?.error && replyText ? [{ title: `${toolName}结果`, content: replyText }] : []); if (ragItems.length > 0) { let speechText = normalizeTextForSpeech(replyText); session.handoffSummaryUsed = true; if (toolName === 'search_knowledge' && speechText) { const cleanedText = speechText.replace(/^(根据知识库信息[,,::\s]*|根据.*?[,,]\s*)/i, ''); return { delivery: 'external_rag', speechText: '', ragItems: [{ title: '知识库结果', content: cleanedText || speechText }], source, toolName, routeDecision, responseMeta, }; } return { delivery: 'external_rag', speechText: '', ragItems, source, toolName, routeDecision, responseMeta, }; } if (toolName === 'search_knowledge' && !toolResult?.hit) { session.handoffSummaryUsed = true; // 敏感问题(传销/正规性)知识库未命中时,不交给S2S自由发挥,直接返回安全回复 if (/(传销|骗局|骗子|正规吗|合法吗|正不正规|合不合法|是不是传销|直销还是传销|层级分销|非法集资|拉人头|下线|发展下线|报单|人头费)/.test(originalText)) { const safeReply = '德国PM是一家1993年成立于德国的合法直销公司,获得邓白氏AAA+认证,业务覆盖100多个国家和地区。它不是传销,是正规的直销企业哦。如果你想了解更多,可以问我关于PM公司或产品的详细介绍。'; return { delivery: 'external_rag', speechText: '', ragItems: [{ title: '品牌保护', content: safeReply }], source: 'voice_tool', toolName: 'search_knowledge', routeDecision, responseMeta: { ...responseMeta, hit: true, reason: 'brand_protection' }, }; } return { delivery: 'upstream_chat', speechText: '', ragItems: [], source: 'voice_bot', toolName: null, routeDecision, responseMeta, }; } } const speechText = normalizeTextForSpeech(replyText); session.handoffSummaryUsed = true; if (!speechText) { return { delivery: 'local_tts', speechText: '', ragItems: [], source, toolName, routeDecision, responseMeta }; } return { delivery: 'local_tts', speechText, ragItems: [], source, toolName, routeDecision, responseMeta }; } module.exports = { getRuleBasedDirectRouteDecision, normalizeKnowledgeAlias, normalizeTextForSpeech, splitTextForSpeech, estimateSpeechDurationMs, shouldForceKnowledgeRoute, resolveReply, };