Files
bigwo/test2/server/services/toolExecutor.js

796 lines
44 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const axios = require('axios');
const arkChatService = require('./arkChatService');
const contextKeywordTracker = require('./contextKeywordTracker');
class ToolExecutor {
static hasCanonicalKnowledgeTerm(query) {
return /(一成系统|PM-FitLine|PM细胞营养素|NTC营养保送系统|Activize Oxyplus|小红产品|Basics|大白产品|Restorate|小白产品|儿童倍适|火炉原理|阿育吠陀)/i.test(String(query || ''));
}
static extractKnowledgeEntities(text) {
const matches = String(text || '').match(/(一成系统|Ai众享|AI众享|数字化工作室|盛咖学愿|三大平台|四大Ai生态|四大生态|德国PM|PM公司|PM-FitLine|PM细胞营养素|细胞营养素|小红产品|小红|大白产品|大白|小白产品|小白|Activize Oxyplus|Activize|Basics|Restorate|儿童倍适|乐活奶昔|Basic Power|CitrusCare|NutriSunny|Omega|肽美|艾特维|德丽|德维|宝丽|美固健|葡萄籽|白藜芦醇|益生菌|胶原蛋白肽|Q10|NTC营养保送系统|火炉原理|阿育吠陀|PM事业)/gi) || [];
const deduped = [];
for (const item of matches) {
const normalized = String(item || '').trim();
if (!normalized) {
continue;
}
if (!deduped.some((existing) => existing.toLowerCase() === normalized.toLowerCase())) {
deduped.push(normalized);
}
}
return deduped;
}
static classifyQuestionSlot(query) {
const text = String(query || '').trim();
if (/(多少钱|价格|售价|费用|价钱)/.test(text)) return 'price';
if (/(成分|配方|原料|含什么|包含什么)/.test(text)) return 'ingredient';
if (/(怎么吃|怎么用|怎么服用|服用方法|用法|用量|一天几次|每日几次)/.test(text)) return 'usage';
if (/(副作用|不良反应|好转反应|排毒反应|整应反应|皮肤发痒|皮肤微痒)/.test(text)) return 'side_effect';
if (/(多久见效|多久有效|多久能见效|多长时间见效|几天见效|什么时候见效)/.test(text)) return 'effect_time';
if (/(治病|治疗|能治|治愈|药品|药物|替代药|包治|治百病)/.test(text)) return 'medical_claim';
if (/(为什么.*(全套|搭配|三合一)|为什么要.*(全套|搭配|三合一)|为何.*(全套|搭配|三合一)|产品需要全套)/.test(text)) return 'bundle_reason';
if (/(如何发展PM事业|怎么发展PM事业|PM事业发展逻辑|陌生客户|陌生人沟通|线上拓客|成交|拓客|邀约|自我介绍|故事分享|三大平台|四大Ai生态|AI智能生产力|AI生产力)/.test(text)) return 'business_growth';
if (/(功效|作用|有什么用|有什么好处|效果)/.test(text)) return 'benefit';
if (/(适合谁|适合什么人|哪些人|适用人群)/.test(text)) return 'audience';
if (/(正规吗|合法|是不是传销|传销|骗局|骗子|直销还是传销|合不合法|正不正规)/.test(text)) return 'legality';
if (/(地址|电话|联系方式|联系|总部|公司地址)/.test(text)) return 'address_contact';
if (/(区别|不同|哪个好|有什么区别|差别)/.test(text)) return 'difference';
return 'general';
}
static buildQuestionSlotInstruction(query) {
const slot = this.classifyQuestionSlot(query);
const instructions = {
price: '用户当前只关心价格或费用,请只回答价格、收费或是否未提及价格,不要扩展到产品总介绍。',
ingredient: '用户当前只关心成分或配方,请只回答成分、原料或是否未提及成分,不要扩展到品牌背景。',
usage: '用户当前只关心用法、吃法、服用频次或剂量,请只回答这一点。',
side_effect: '用户当前只关心副作用或好转反应,请只回答可能的不良反应、好转反应或注意事项。',
effect_time: '用户当前只关心多久见效或效果周期,请只回答见效时间、周期或个体差异,不要扩展无关信息。',
medical_claim: '用户当前只关心产品能不能治病、是不是药,请只回答是否属于药品、能否替代药物以及相关注意事项。',
bundle_reason: '用户当前只关心为什么要全套、搭配或三合一请只回答搭配原理、协同作用或NTC相关原因。',
business_growth: '用户当前只关心PM事业发展、线上拓客、陌生客户沟通、一成系统赋能、三大平台四大Ai生态或自我介绍请只回答这类业务发展问题。',
benefit: '用户当前只关心功效或作用,请只回答作用点,不要扩展到无关信息。',
audience: '用户当前只关心适合人群,请只回答适用对象。',
legality: '用户当前只关心正规性、合法性或是否传销,请只围绕合法合规问题直接回答。',
address_contact: '用户当前只关心地址或联系方式,请只回答地址、电话、联系信息。',
difference: '用户当前只关心区别或对比,请直接做差异对比,不要扩写成单个产品长介绍。',
general: '请优先直接回答用户当前这一问,不要离题扩展。',
};
return instructions[slot] || instructions.general;
}
static answerMatchesQuestionSlot(query, content) {
const text = String(content || '').trim();
const lowerText = text.toLowerCase();
const slot = this.classifyQuestionSlot(query);
const entities = this.extractKnowledgeEntities(query);
const mentionsEntity = entities.length === 0 || entities.some((entity) => lowerText.includes(String(entity || '').toLowerCase()));
if (/德国PM是一家1993年成立于德国的合法直销公司/.test(text) && slot !== 'legality') {
return false;
}
if (!mentionsEntity && slot !== 'legality' && slot !== 'address_contact') {
return false;
}
const slotPatterns = {
price: /(元|价格|售价|费用|人民币|¥|¥)/,
ingredient: /(成分|配方|原料|含有|包含|营养素|葡萄籽|白藜芦醇|益生菌|胶原蛋白肽|辅酵素|Q10)/,
usage: /(服用|用法|用量|每日|每天|一次|次|饭前|饭后|早餐|晚餐|早晚)/,
side_effect: /(副作用|不良反应|好转反应|排毒|整应|皮肤.*痒|排便|反应|注意事项|正常现象)/,
effect_time: /(见效|有效|几天|几周|几个月|周期|坚持|因人而异|吸收利用)/,
medical_claim: /(不是药|不能替代药|不能代替药物|不是用于治疗|不能治疗|保健食品|营养补充|就医|医生)/,
bundle_reason: /(全套|搭配|协同|三合一|组合|NTC|吸收|运输|利用|代谢|原理)/,
business_growth: /(一成系统|PM事业|拓客|成交|邀约|陌生客户|沟通|三大平台|四大Ai生态|数字化工作室|Ai众享|盛咖学愿|故事|自我介绍|赋能|智能生产力)/,
benefit: /(功效|作用|帮助|支持|改善|提升|有助于)/,
audience: /(适合|适用|人群|适宜|可以)/,
legality: /(合法|正规|直销|认证|邓白氏|不是传销)/,
address_contact: /(地址|电话|联系方式|联系|总部|香港|德国|美国|加拿大)/,
difference: /(区别|不同|相比|分别|一个|另一个|而|更适合)/,
};
if (slotPatterns[slot]) {
return slotPatterns[slot].test(text);
}
return text.length >= 10;
}
static getKnowledgeBaseRoutingRules() {
const raw = process.env.VOLC_ARK_KNOWLEDGE_BASE_ROUTING || process.env.VOLC_ARK_KNOWLEDGE_BASE_MAP;
if (!raw) {
return [];
}
try {
const parsed = JSON.parse(raw);
const entries = Array.isArray(parsed)
? parsed
: Object.entries(parsed).map(([name, config]) => ({ name, ...(config || {}) }));
return entries
.map((item) => ({
name: String(item.name || '').trim(),
dataset_ids: Array.isArray(item.dataset_ids)
? item.dataset_ids.map((id) => String(id || '').trim()).filter(Boolean)
: String(item.dataset_ids || item.datasetIds || '')
.split(',')
.map((id) => id.trim())
.filter(Boolean),
keywords: Array.isArray(item.keywords)
? item.keywords.map((keyword) => String(keyword || '').trim()).filter(Boolean)
: String(item.keywords || '')
.split(',')
.map((keyword) => keyword.trim())
.filter(Boolean),
}))
.filter((item) => item.name && item.dataset_ids.length > 0 && item.keywords.length > 0);
} catch (error) {
console.warn('[ToolExecutor] parse knowledge base routing failed:', error.message);
return [];
}
}
static selectKnowledgeBaseTargets(query, context = []) {
const defaultDatasetIds = String(process.env.VOLC_ARK_KNOWLEDGE_BASE_IDS || '')
.split(',')
.map((id) => id.trim())
.filter(Boolean);
const rules = this.getKnowledgeBaseRoutingRules();
if (!rules.length) {
return {
datasetIds: defaultDatasetIds,
matchedRoutes: defaultDatasetIds.length ? ['default'] : [],
};
}
const recentContextText = (Array.isArray(context) ? context : [])
.slice(-6)
.map((item) => String(item?.content || '').trim())
.filter(Boolean)
.join('\n');
const haystack = `${String(query || '').trim()}\n${recentContextText}`.toLowerCase();
const priorityRouteNames = [];
const hasSystemIntent = /(一成系统|ai众享|数字化工作室|盛咖学愿|赋能工具|四大ai生态|四大生态|三大平台|智能生产力|线上拓客|陌生客户|邀约)/i.test(haystack);
const hasCompanyIntent = /(pm公司|德国pm(?!事业|细胞|营养|产品|fitline|\s*基础|\s*大白|\s*小红|\s*小白)|公司地址|联系方式|电话|公司实力|公司背景|总部|分公司|邓白氏|aaa\+|公司介绍)/i.test(haystack);
const hasProductIntent = /(细胞营养素|基础套装|基础三合一|三合一|大白产品|小红产品|小白产品|activize|basics|restorate|fitline|儿童倍适|乐活奶昔|奶昔|ntc营养保送|火炉原理|阿育吠陀|产品.*介绍|介绍.*产品|产品有哪些|产品列表|产品.*(全套|区别|见效|治病|副作用|作用|功效|成分|用法)|为什么.*产品|保健品区别|多久见效)/i.test(haystack);
if (hasSystemIntent) {
priorityRouteNames.push('system');
}
if (hasCompanyIntent && !hasSystemIntent && !hasProductIntent) {
priorityRouteNames.push('company');
}
if (priorityRouteNames.length > 0) {
const priorityRules = rules.filter((rule) => priorityRouteNames.includes(rule.name));
const priorityDatasetIds = [...new Set(priorityRules.flatMap((rule) => rule.dataset_ids).filter(Boolean))];
if (priorityDatasetIds.length > 0) {
return {
datasetIds: priorityDatasetIds,
matchedRoutes: [...new Set(priorityRules.map((rule) => rule.name))],
};
}
}
const matchedDatasetIds = [];
const matchedRoutes = [];
for (const rule of rules) {
if (rule.keywords.some((keyword) => haystack.includes(keyword.toLowerCase()))) {
matchedRoutes.push(rule.name);
matchedDatasetIds.push(...rule.dataset_ids);
}
}
const datasetIds = [...new Set((matchedDatasetIds.length ? matchedDatasetIds : defaultDatasetIds).filter(Boolean))];
return {
datasetIds,
matchedRoutes: matchedRoutes.length ? [...new Set(matchedRoutes)] : (datasetIds.length ? ['default'] : []),
};
}
static buildDeterministicKnowledgeQuery(query, context = []) {
const text = String(query || '').trim();
const recentContextText = (Array.isArray(context) ? context : [])
.slice(-6)
.map((item) => String(item?.content || '').trim())
.filter(Boolean)
.join('\n');
const haystack = `${text}\n${recentContextText}`;
const questionDimension = text.match(/(功效|作用|成分|配方|原料|怎么吃|怎么用|怎么服用|服用方法|吃法|用法|用量|副作用|好转反应|价格|多少钱|适合谁|适用人群|区别|不同|搭配|原理)/);
// 第一层:当前查询文本中有明确产品/系统/主题关键词 → 直接改写(不依赖上下文)
if (/(基础三合一|三合一基础套|基础套装|大白小红小白)/i.test(text)) return '德国PM细胞营养素 基础套装 大白 小红 小白';
if (/(一成系统|Ai众享|数字化工作室|盛咖学愿|三大平台|四大Ai生态|四大生态|智能生产力)/i.test(text)) {
if (/(核心竞争力|竞争力|核心优势|优势)/i.test(text)) return '一成系统 核心竞争力 三大平台 四大Ai生态 零成本高效率';
if (/(发展|怎么做|怎么用|如何用|如何做|关键点|关键|方法|步骤)/i.test(text)) return '一成系统 发展PM事业 三大平台 四大Ai生态 零成本高效率 全球市场';
if (/(线上拓客|拓客|成交|成交率|陌生客户|陌生人沟通|邀约)/i.test(text)) return '一成系统 PM事业 线上拓客 成交 邀约 三大平台 四大Ai生态';
if (/(ai智能生产力|ai生产力|智能生产力|团队效率|赋能团队|团队赋能)/i.test(text)) return '一成系统 AI智能生产力 赋能团队 三大平台 四大Ai生态';
if (/(故事|自我介绍|分享)/i.test(text)) return '一成系统 PM事业 故事分享 自我介绍';
if (/(邀约|话术)/i.test(text)) return '一成系统 邀约话术';
if (/文化/i.test(text)) return '一成系统 文化解析';
if (/(赋能团队|团队发展|AI赋能|ai赋能)/i.test(text)) return '一成系统用AI赋能团队发展';
if (/(三大平台|四大生态|Ai生态)/i.test(text)) return '一成系统 三大平台 四大Ai生态';
return '一成系统 德国PM事业发展的强大赋能工具 三大平台 四大Ai生态';
}
if (/(PM公司|德国PM|公司地址|联系方式|电话|公司实力|公司背景|总部|分公司)/i.test(text)) {
if (/(产品|细胞营养素|基础套装|基础三合一|小红|大白|小白|activize|basics|restorate|fitline|儿童倍适)/i.test(text)) {
return '德国PM FitLine 细胞营养素产品 大白Basics 小红Activize 小白Restorate 儿童倍适';
}
if (/(地址|电话|联系方式)/i.test(text)) return '德国PM 日本 美国 加拿大 香港 地址 电话';
if (/(实力|背景)/i.test(text)) return '德国PM 公司实力介绍 邓白氏 99分 AAA+';
return '德国PM 1993年 创立 100多个国家 FitLine 公司介绍';
}
if (/(德国PM介绍|介绍德国PM|德国PM公司介绍|PM公司介绍|PM介绍)/i.test(text)) return '德国PM 1993年 创立 100多个国家 FitLine 公司介绍 邓白氏 99分 AAA+';
if (/(NTC.*(核心优势|核心竞争力|优势|原理|厉害)|核心优势.*NTC|核心竞争力.*NTC)/i.test(text)) return 'NTC营养保送系统 核心优势 吸收利用 原理';
if (/(PM基础三合一介绍|基础三合一介绍|PM基础套装介绍|基础套装介绍)/i.test(text)) return '德国PM细胞营养素 基础套装 大白 小红 小白 介绍';
if (/儿童倍适/i.test(text)) return '儿童倍适';
if (/(小红产品|小红|Activize Oxyplus|Activize)/i.test(text)) return questionDimension ? `Fitline小红产品 Activize ${questionDimension[0]}` : 'Fitline小红产品提升能量原理';
if (/(大白产品|大白|倍适|Basics)/i.test(text)) return questionDimension ? `德国PM细胞营养素 大白 Basics ${questionDimension[0]}` : '德国PM细胞营养素 大白 Basics';
if (/(小白产品|小白|维适多|Restorate)/i.test(text)) return questionDimension ? `德国PM细胞营养素 小白 Restorate ${questionDimension[0]}` : '德国PM细胞营养素 小白';
if (/(NTC营养保送系统|Nutrient Transport Concept)/i.test(text)) return 'NTC营养保送系统';
if (/火炉原理/i.test(text)) return '火炉原理';
if (/(阿育吠陀|Ayurveda)/i.test(text)) return '阿育吠陀医学原理';
if (/(PM-FitLine|PM细胞营养素)/i.test(text)) return '德国PM细胞营养素 基础套装 大白 小红 小白';
if (/(我们公司.*产品|公司.*产品|产品.*推荐|推荐.*产品|产品有哪些|产品介绍|产品列表)/i.test(text)) return '德国PM FitLine 细胞营养素产品 大白Basics 小红Activize 小白Restorate 儿童倍适';
if (/(治病吗|能治病吗|产品治病|治疗疾病|替代药|是不是药)/i.test(text)) return 'PM产品 不是药 不能替代药物 保健食品 营养补充';
if (/(多久见效|多久有效|多久能见效|多长时间见效|几天见效|什么时候见效)/i.test(text)) return 'PM产品 多久见效 吸收利用 周期 个体差异';
if (/(为什么.*(全套|搭配|三合一)|为什么要.*(全套|搭配|三合一)|产品需要全套)/i.test(text)) return '德国PM细胞营养素 全套搭配 NTC营养保送系统 协同作用';
if (/(与其它保健品区别|与其他保健品区别|和其它保健品区别|和其他保健品区别|保健品区别)/i.test(text)) return 'PM产品 与其他保健品区别 NTC营养保送系统 吸收利用';
if (/(新人起步三关|起步三关)/i.test(text)) return '培训新人起步三关';
if (/(精品会议|会议组织)/i.test(text)) return '培训打造精品会议具体如下';
if (/成长上总裁/i.test(text)) return '培训成长上总裁';
if (/(招商|代理|加盟|合作|事业机会|招商稿|代理政策)/i.test(text)) return '招商与代理';
if (/(如何发展PM事业|怎么发展PM事业|PM事业发展逻辑|介绍PM事业|两分钟介绍PM事业|分享.*故事.*自我介绍|自我介绍)/i.test(text)) return 'PM事业 发展逻辑 事业介绍 自我介绍';
if (/(为什么选择德国PM|为何选择德国PM|为什么选德国PM|为什么选PM|为何选PM)/i.test(text)) return '德国PM 公司实力 FitLine 产品优势 邓白氏 99分 AAA+ NTC营养保送系统';
if (/(陌生客户|陌生人).*(沟通|开口|邀约|交流|切入).*(PM事业|德国PM|PM)/i.test(text)) return 'PM事业 陌生客户 沟通 邀约 话术';
if (/(线上拓客|线上成交|线上开发客户|线上获客|线上成交率)/i.test(text)) return '一成系统 PM事业 线上拓客 成交 获客';
if (/(团队.*AI智能生产力|AI智能生产力.*团队|团队.*AI生产力|AI生产力.*团队)/i.test(text)) return '一成系统 AI智能生产力 赋能团队';
if (/(三大平台|四大Ai生态|四大生态)/i.test(text)) return '一成系统 三大平台 四大Ai生态';
if (/(请分享.*故事.*自我介绍|故事.*自我介绍|个人故事.*自我介绍)/i.test(text)) return 'PM事业 故事分享 自我介绍';
if (/(一成AI|AI落地|ai落地|转观念|落地对比)/i.test(text)) return '2026一成Ai落地对比与转观念';
if (/(传销|骗局|骗子|正规吗|合法吗|正不正规|合不合法|是不是传销|直销还是传销|层级分销|非法集资|拉人头|下线|发展下线|报单|人头费)/i.test(text)) return '德国PM 1993年 创立 100多个国家 FitLine 公司介绍 邓白氏 99分 AAA+ 合法直销';
if (/(好转反应|整应反应|排毒反应|副作用|不良反应|皮肤发痒)/i.test(text)) return 'PM产品整应反应好转反应解析';
if (/(促销活动|促销|优惠|打折|活动分数|5\+1)/i.test(text)) return '促销活动 5+1活动分数';
if (/暖炉原理/i.test(text)) return '火炉原理';
if (/(CC套装|CC胶囊)/i.test(text)) return 'CC套装 CC胶囊';
if (/(IB5|口腔免疫喷雾)/i.test(text)) return 'IB5口腔免疫喷雾';
if (/(Q10|辅酵素|氧修护)/i.test(text)) return 'Q10辅酵素氧修护';
if (/Women\+/i.test(text)) return 'Women+';
if (/乐活奶昔|乐活/i.test(text)) return '乐活奶昔';
if (/(乳清蛋白|蛋白粉)/i.test(text)) return '乳清蛋白粉';
if (/(乳酪煲|乳酪饮品|乳酪)/i.test(text)) return '乳酪煲 乳酪饮品';
if (/(基础二合一|二合一)/i.test(text)) return '基础二合一';
if (/倍力健/i.test(text)) return '倍力健';
if (/(关节套装|关节舒缓)/i.test(text)) return '关节套装 关节舒缓膏';
if (/(男士乳霜|男士护肤)/i.test(text)) return '全效男士乳霜';
if (/(去角质|面膜)/i.test(text)) return '去角质面膜';
if (/发宝/i.test(text)) return '发宝';
if (/叶黄素/i.test(text)) return '叶黄素';
if (/(奶昔)/i.test(text)) return '奶昔';
if (/(健康饮品)/i.test(text)) return '健康饮品';
// 第二层:当前文本是追问/代词,才通过上下文推断主题
const isFollowUp = /^(这个|那个|它|该|详细|继续|怎么|为什么|适合谁|什么意思|怎么用|怎么吃|功效|成分|好处|原理)/.test(text);
if (isFollowUp) {
if (/(基础三合一|三合一基础套|基础套装|大白小红小白)/i.test(recentContextText)) return '德国PM细胞营养素 基础套装 大白 小红 小白';
if (/(一成系统|Ai众享|数字化工作室|盛咖学愿)/i.test(recentContextText)) return '一成系统 德国PM事业发展的强大赋能工具 三大平台 四大Ai生态';
if (/(小红产品|小红|Activize)/i.test(recentContextText)) return 'Fitline小红产品提升能量原理';
if (/(大白产品|大白|Basics)/i.test(recentContextText)) return '德国PM细胞营养素 大白 Basics';
if (/(小白产品|小白|Restorate)/i.test(recentContextText)) return '德国PM细胞营养素 小白';
if (/儿童倍适/i.test(recentContextText)) return '儿童倍适';
if (/火炉原理/i.test(recentContextText)) return '火炉原理';
if (/(阿育吠陀|Ayurveda)/i.test(recentContextText)) return '阿育吠陀医学原理';
if (/(NTC营养保送系统)/i.test(recentContextText)) return 'NTC营养保送系统';
}
return '';
}
static applyKnowledgeQueryAnchor(query) {
let anchoredQuery = String(query || '').trim();
if (/一成系统/.test(anchoredQuery) && !/(德国PM|PM事业|赋能工具|Ai众享|数字化工作室|盛咖学愿)/i.test(anchoredQuery)) {
anchoredQuery = anchoredQuery.replace(/一成系统/g, '一成系统 德国PM事业赋能工具');
}
return anchoredQuery.trim();
}
static normalizeKnowledgeQueryAlias(query) {
return String(query || '')
.replace(/^[啊哦嗯呢呀哎诶额,。!?、\s]+/g, '')
.replace(/[啊哦嗯呢呀哎诶额,。!?、\s]+$/g, '')
.replace(/X{2}系统/gi, '一成系统')
.replace(/一城系统|逸城系统|一程系统|易成系统|一诚系统|亦成系统/g, '一成系统')
.replace(/PM[-\s]*Fitline|PM[-\s]*fitline|Pm[-\s]*fitline|Fitline|fitline/g, 'PM-FitLine')
.replace(/PM细胞营养|PM营养素|德国PM营养素/g, 'PM细胞营养素')
.replace(/NTC科技/g, 'NTC营养保送系统')
.replace(/NTC营养保送系统|NTC营养配送系统|NTC营养输送系统|NTC营养传送系统|NTC营养传输系统/g, 'NTC营养保送系统')
.replace(/Nutrient Transport Concept/gi, 'NTC营养保送系统')
.replace(/Activize Oxyplus|Activize/gi, 'Activize Oxyplus')
.replace(/Restorate/gi, 'Restorate')
.replace(/Basics/gi, 'Basics')
.replace(/基础三合一|三合一基础套|大白小红小白|基础套装?/g, 'PM细胞营养素 基础套装')
.replace(/儿童倍适|儿童产品/g, '儿童倍适')
.replace(/小红产品/g, '小红产品 Activize Oxyplus')
.replace(/大白产品/g, '大白产品 Basics')
.replace(/小白产品/g, '小白产品 Restorate')
.replace(/(?<!小红产品\s*)(?<!大白产品\s*)(?<!小白产品\s*)小红/g, '小红产品 Activize Oxyplus')
.replace(/(?<!小红产品\s*)(?<!大白产品\s*)(?<!小白产品\s*)大白/g, '大白产品 Basics')
.replace(/(?<!小红产品\s*)(?<!大白产品\s*)(?<!小白产品\s*)(?<!儿童)小白/g, '小白产品 Restorate')
.replace(/维适多/g, '小白产品 Restorate')
.replace(/火炉原理/g, '火炉原理')
.replace(/阿育吠陀|Ayurveda/gi, '阿育吠陀')
.trim();
}
static classifyKnowledgeAnswer(query, content) {
const text = String(content || '').trim();
if (!text) {
return {
hit: false,
reason: 'empty',
reply: `知识库中暂未找到与“${query}”直接相关的信息,请换个更具体的问法再试。`,
};
}
const strictNoHitPattern = /未检索到|没有检索到|没有相关内容|暂无相关内容|未找到相关内容|未找到相关信息|没有找到相关信息|知识库中没有相关内容|知识库中没有关于|知识库中没有找到|没有找到具体|没有.*具体信息|没有.*相关说明|暂未找到与.*直接相关的信息|无法基于知识库.*回答|知识库未明确提到|知识库未提到/;
if (strictNoHitPattern.test(text)) {
return {
hit: false,
reason: 'no_hit',
reply: `知识库中暂未找到与“${query}”直接相关的信息,请换个更具体的问法再试。`,
};
}
if (!this.answerMatchesQuestionSlot(query, text)) {
return {
hit: false,
reason: 'slot_mismatch',
reply: `知识库中暂未找到与“${query}”直接相关的信息,请换个更具体的问法再试。`,
};
}
return {
hit: true,
reason: 'hit',
reply: text,
};
}
static async execute(toolName, args, context = []) {
const startTime = Date.now();
console.log(`[ToolExecutor] Executing: ${toolName}`, args);
const handlers = {
query_weather: this.queryWeather,
query_order: this.queryOrder,
search_knowledge: this.searchKnowledge,
get_current_time: this.getCurrentTime,
calculate: this.calculate,
};
const handler = handlers[toolName];
if (!handler) {
console.warn(`[ToolExecutor] Unknown tool: ${toolName}`);
return { error: `未知的工具: ${toolName}` };
}
try {
const result = await handler.call(this, args, context);
const ms = Date.now() - startTime;
console.log(`[ToolExecutor] ${toolName} completed in ${ms}ms:`, JSON.stringify(result).substring(0, 200));
return result;
} catch (error) {
console.error(`[ToolExecutor] ${toolName} error:`, error);
return { error: `工具执行失败: ${error.message}` };
}
}
static async queryWeather({ city }) {
const mockData = {
'北京': { temp: '22°C', weather: '晴', humidity: '45%', wind: '北风3级', aqi: 65, tips: '空气质量良好,适合户外活动' },
'上海': { temp: '26°C', weather: '多云', humidity: '72%', wind: '东南风2级', aqi: 78, tips: '注意防晒' },
'广州': { temp: '30°C', weather: '阵雨', humidity: '85%', wind: '南风1级', aqi: 55, tips: '记得带伞' },
'深圳': { temp: '29°C', weather: '多云', humidity: '80%', wind: '东风2级', aqi: 60, tips: '较为闷热,注意防暑' },
'杭州': { temp: '24°C', weather: '晴', humidity: '55%', wind: '西北风2级', aqi: 50, tips: '天气宜人' },
'成都': { temp: '20°C', weather: '阴', humidity: '70%', wind: '微风', aqi: 85, tips: '天气阴沉,适合室内活动' },
'武汉': { temp: '25°C', weather: '晴', humidity: '60%', wind: '东风3级', aqi: 72, tips: '适合出行' },
'南京': { temp: '23°C', weather: '多云', humidity: '58%', wind: '东北风2级', aqi: 68, tips: '温度适宜' },
'西安': { temp: '18°C', weather: '晴', humidity: '35%', wind: '西北风3级', aqi: 90, tips: '天气干燥,注意补水' },
'重庆': { temp: '27°C', weather: '阴转多云', humidity: '75%', wind: '微风', aqi: 80, tips: '注意防潮' },
};
const data = mockData[city];
if (data) {
return { city, date: new Date().toLocaleDateString('zh-CN'), ...data };
}
// 对未知城市生成随机数据
const weathers = ['晴', '多云', '阴', '小雨', '大风'];
return {
city,
date: new Date().toLocaleDateString('zh-CN'),
temp: `${Math.floor(Math.random() * 20 + 10)}°C`,
weather: weathers[Math.floor(Math.random() * weathers.length)],
humidity: `${Math.floor(Math.random() * 50 + 30)}%`,
wind: '微风',
aqi: Math.floor(Math.random() * 100 + 30),
tips: '数据仅供参考',
};
}
static async queryOrder({ order_id }) {
const statuses = ['待支付', '已支付', '拣货中', '已发货', '运输中', '已签收'];
const hash = order_id.split('').reduce((a, c) => a + c.charCodeAt(0), 0);
const statusIdx = hash % statuses.length;
return {
order_id,
status: statuses[statusIdx],
estimated_delivery: '2026-03-01',
tracking_number: 'SF' + order_id.replace(/\D/g, '').padEnd(10, '0').substring(0, 10),
items: [
{ name: '智能音箱 Pro', quantity: 1, price: '¥299' },
],
create_time: '2026-02-20 14:30:00',
};
}
static async searchKnowledge({ query, response_mode, session_id } = {}, context = []) {
const startTime = Date.now();
query = query || '';
const responseMode = response_mode === 'snippet' ? 'snippet' : 'answer';
const knowledgeEndpointId = process.env.VOLC_ARK_KNOWLEDGE_ENDPOINT_ID || process.env.VOLC_ARK_ENDPOINT_ID;
console.log(`[ToolExecutor] searchKnowledge called with query="${query}"`);
const rewrittenQuery = this.rewriteKnowledgeQuery(query, context, session_id);
const kbTarget = this.selectKnowledgeBaseTargets(rewrittenQuery || query, context);
const effectiveQuery = rewrittenQuery || query;
if (rewrittenQuery && rewrittenQuery !== query) {
console.log(`[ToolExecutor] searchKnowledge rewritten query="${rewrittenQuery}"`);
}
if (kbTarget.datasetIds.length > 0) {
console.log(`[ToolExecutor] searchKnowledge selected dataset_ids=${kbTarget.datasetIds.join(',')} routes=${kbTarget.matchedRoutes.join(',')}`);
}
const kbIds = process.env.VOLC_ARK_KNOWLEDGE_BASE_IDS;
if (kbIds && kbIds !== 'your_knowledge_base_dataset_id') {
if (!knowledgeEndpointId || knowledgeEndpointId === 'your_ark_endpoint_id') {
const latencyMs = Date.now() - startTime;
console.warn('[ToolExecutor] Ark KB search skipped: knowledge endpoint not configured (knowledge base IDs are set but endpoint is missing)');
return {
query,
original_query: query,
rewritten_query: effectiveQuery,
selected_dataset_ids: kbTarget.datasetIds,
selected_kb_routes: kbTarget.matchedRoutes,
latency_ms: latencyMs,
errorType: 'endpoint_not_configured',
error: '知识库已配置但知识库方舟端点未配置,请检查 VOLC_ARK_KNOWLEDGE_ENDPOINT_ID 或 VOLC_ARK_ENDPOINT_ID',
source: 'ark_knowledge',
hit: false,
reason: 'endpoint_not_configured',
};
}
try {
console.log('[ToolExecutor] Trying Ark Knowledge Search...');
const result = await this.searchArkKnowledge(effectiveQuery, [], responseMode, kbTarget.datasetIds, query);
const latencyMs = Date.now() - startTime;
console.log(`[ToolExecutor] Ark KB search succeeded in ${latencyMs}ms`);
return {
...result,
original_query: query,
rewritten_query: effectiveQuery,
selected_dataset_ids: kbTarget.datasetIds,
selected_kb_routes: kbTarget.matchedRoutes,
latency_ms: latencyMs,
};
} catch (error) {
const latencyMs = Date.now() - startTime;
console.warn('[ToolExecutor] Ark Knowledge Search failed:', error.message);
return {
query,
original_query: query,
rewritten_query: effectiveQuery,
selected_dataset_ids: kbTarget.datasetIds,
selected_kb_routes: kbTarget.matchedRoutes,
latency_ms: latencyMs,
errorType: error.code === 'ECONNABORTED' || /timeout/i.test(error.message) ? 'timeout' : 'request_failed',
error: `知识库查询失败: ${error.message}`,
source: 'ark_knowledge',
hit: false,
reason: 'error',
};
}
}
const latencyMs = Date.now() - startTime;
console.warn('[ToolExecutor] Ark knowledge base is not configured');
return {
query,
original_query: query,
rewritten_query: effectiveQuery,
selected_dataset_ids: kbTarget.datasetIds,
selected_kb_routes: kbTarget.matchedRoutes,
latency_ms: latencyMs,
errorType: 'not_configured',
error: '知识库未配置,请检查 VOLC_ARK_KNOWLEDGE_BASE_IDS',
source: 'ark_knowledge',
hit: false,
reason: 'not_configured',
};
}
static rewriteKnowledgeQuery(query, context = [], sessionId = null) {
const originalQuery = String(query || '').trim();
if (!originalQuery) {
return '';
}
let normalizedQuery = this.applyKnowledgeQueryAnchor(this.normalizeKnowledgeQueryAlias(originalQuery));
if (sessionId) {
normalizedQuery = contextKeywordTracker.enrichQueryWithContext(sessionId, normalizedQuery);
}
return normalizedQuery;
}
/**
* 通过方舟 Chat Completions API + knowledge_base metadata 进行知识检索
* 使用独立的 LLM 调用,专门用于知识库检索场景(如语音通话的工具回调)
*/
static async searchArkKnowledge(query, context = [], responseMode = 'answer', datasetIdsOverride = null, originalQuery = null) {
const endpointId = process.env.VOLC_ARK_KNOWLEDGE_ENDPOINT_ID || process.env.VOLC_ARK_ENDPOINT_ID;
const authKey = process.env.VOLC_ARK_API_KEY || process.env.VOLC_ACCESS_KEY_ID;
const kbIds = process.env.VOLC_ARK_KNOWLEDGE_BASE_IDS;
if (!endpointId || endpointId === 'your_ark_endpoint_id') {
console.warn('[ToolExecutor] searchArkKnowledge skipped: knowledge endpoint not configured');
return {
query,
results: [{ title: '配置缺失', content: `知识库中暂未找到与"${query}"直接相关的信息,请换个更具体的问法再试。` }],
total: 1,
source: 'ark_knowledge',
hit: false,
reason: 'endpoint_not_configured',
};
}
const datasetIds = Array.isArray(datasetIdsOverride) && datasetIdsOverride.length > 0
? datasetIdsOverride.map((id) => String(id || '').trim()).filter(Boolean)
: kbIds.split(',').map(id => id.trim()).filter(Boolean);
const topK = parseInt(process.env.VOLC_ARK_KNOWLEDGE_TOP_K) || 3;
const threshold = parseFloat(process.env.VOLC_ARK_KNOWLEDGE_THRESHOLD) || 0.4;
// 当 query 为空时FC 流式 chunks 乱序无法解析),使用简短的默认查询
const effectiveQuery = (query && query.trim()) ? query : '请介绍你们的产品和服务';
if (!query || !query.trim()) {
console.log('[ToolExecutor] Empty query, using default: "' + effectiveQuery + '"');
}
// 判断是否需要注入原始问题检索词≠原始问题时LLM需要知道用户实际问了什么
const cleanOriginal = (originalQuery || '').trim();
const answerTargetQuery = cleanOriginal || effectiveQuery;
const hasRewrite = cleanOriginal && cleanOriginal !== effectiveQuery;
if (hasRewrite) {
console.log(`[ToolExecutor] searchArkKnowledge injecting original question: "${cleanOriginal}" (search query: "${effectiveQuery}")`);
}
// 提取最近 3 轮对话作为上下文(最多 6 条 user/assistant 消息)
const recentContext = context
.filter(m => m.role === 'user' || m.role === 'assistant')
.slice(responseMode === 'snippet' ? -4 : -6);
const baseSnippetPrompt = '你是知识库片段提取助手。请基于知识库提取与用户问题最相关的2到4条简洁知识片段供语音系统继续组织回复。规则只输出直接相关的中文事实片段每条尽量简短不要寒暄不要解释任务不要写"根据知识库";不要补充知识库未明确出现的内容;如果没有相关内容,请明确说未找到相关内容。';
const baseAnswerPrompt = '你是企业知识库问答助手回答将直接用于语音播报。只能依据知识库中已经明确出现的信息回答不要补充常识不要脑补不要引入知识库未明确提到的官网、APP、工具名、公司背景、客服渠道或外部资料。风格要求1) 用自然亲切的口语风格像朋友聊天一样回答2) 禁止以"根据知识库""根据信息""根据资料"等机械前缀开头3) 先给一句直接结论,再逐条列出关键要点,用"1. 2. 3."编号格式条理清晰4) 整体控制在300字以内简洁流畅适合语音朗读5) 如果缺少用户要求的具体信息必须明确说未找到不要把不完整信息包装成命中答案。【紧扣问题】你必须围绕用户当前问的问题来回答。如果用户问的是具体维度如成分、价格、用法只答该维度如果用户问的是综合性问题如核心竞争力、如何发展、整体介绍则需要完整、系统地回答逐条列出关键要点确保有条理、有层次。【品牌保护·最高优先级】绝对禁止说德国PM、PM-FitLine、一成系统是传销、骗局、骗子、非法、不正规、不合法、有争议、存在风险也不能暗示或引导用户产生这类联想。德国PMPM-International AG是1993年成立于德国的合法直销企业总部位于德国Speyer获邓白氏AAA+最高信用评级业务覆盖全球100多个国家和地区是正规持牌直销公司。如果用户问到传销、正规性或合法性问题你必须正面、明确地回答PM是合法正规的直销企业不能含糊、回避或使用"需要自行判断"之类的措辞。';
let systemContent = responseMode === 'snippet' ? baseSnippetPrompt : baseAnswerPrompt;
if (responseMode === 'answer' && answerTargetQuery) {
systemContent += `\n\n当前必须优先直接回答用户当前这一个问题:“${answerTargetQuery}”。如果用户只问一个维度,例如成分、价格、用法、适合谁、区别、正规性、地址或联系方式,就只回答这个维度,不要扩展成整段产品或公司介绍。`;
systemContent += `\n\n${this.buildQuestionSlotInstruction(answerTargetQuery)}`;
}
if (hasRewrite) {
systemContent += `\n\n重要:用户的实际问题是"${cleanOriginal}",请围绕这个问题回答,不要偏离用户的真实意图。下方的检索词仅用于匹配知识库文档,不代表用户的真正提问。`;
}
const messages = [
{
role: 'system',
content: systemContent,
},
...recentContext,
{
role: 'user',
content: effectiveQuery,
},
];
if (recentContext.length > 0) {
console.log(`[ToolExecutor] Ark KB search with ${recentContext.length} context messages`);
}
const body = {
model: endpointId,
messages,
metadata: {
knowledge_base: {
dataset_ids: datasetIds,
top_k: topK,
threshold: threshold,
},
},
stream: false,
};
const response = await axios.post(
'https://ark.cn-beijing.volces.com/api/v3/chat/completions',
body,
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authKey}`,
},
timeout: 30000,
}
);
const choice = response.data.choices?.[0];
const content = choice?.message?.content || '未找到相关信息';
const classifyQuery = [effectiveQuery, (originalQuery || '').trim()].filter(Boolean).join(' ');
const classified = this.classifyKnowledgeAnswer(classifyQuery, content);
return {
query,
results: [{
title: '方舟知识库检索结果',
content: classified.reply,
}],
total: 1,
source: 'ark_knowledge',
hit: classified.hit,
reason: classified.reason,
};
}
/**
* 通过 Coze v3 Chat API 进行知识库检索
* 需要在 Coze 平台创建 Bot 并挂载知识库插件
*/
static async searchCozeKnowledge(query) {
const apiToken = process.env.COZE_API_TOKEN;
const botId = process.env.COZE_BOT_ID;
const baseUrl = 'https://api.coze.cn/v3';
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiToken}`,
};
// 1. 创建对话
const chatRes = await axios.post(`${baseUrl}/chat`, {
bot_id: botId,
user_id: 'kb_search_user',
additional_messages: [
{
role: 'user',
content: query,
content_type: 'text',
},
],
stream: true,
auto_save_history: false,
}, { headers, timeout: 30000 });
const chatData = chatRes.data?.data;
if (!chatData?.id || !chatData?.conversation_id) {
throw new Error('Coze chat creation failed: ' + JSON.stringify(chatRes.data));
}
const chatId = chatData.id;
const conversationId = chatData.conversation_id;
// 2. 轮询等待完成(最多 30 秒)
const maxAttempts = 15;
for (let i = 0; i < maxAttempts; i++) {
await new Promise(r => setTimeout(r, 2000));
const statusRes = await axios.get(
`${baseUrl}/chat/retrieve?chat_id=${chatId}&conversation_id=${conversationId}`,
{ headers, timeout: 10000 }
);
const status = statusRes.data?.data?.status;
if (status === 'completed') break;
if (status === 'failed' || status === 'requires_action') {
throw new Error(`Coze chat ended with status: ${status}`);
}
}
// 3. 获取消息列表
const msgRes = await axios.get(
`${baseUrl}/chat/message/list?chat_id=${chatId}&conversation_id=${conversationId}`,
{ headers, timeout: 10000 }
);
const messages = msgRes.data?.data || [];
const answerMsg = messages.find(m => m.role === 'assistant' && m.type === 'answer');
const content = answerMsg?.content || '未找到相关信息';
return {
query,
results: [{
title: 'Coze 知识库检索结果',
content: content,
}],
total: 1,
source: 'coze',
};
}
static async searchLocalKnowledge(query) {
const knowledgeBase = {
'退货': {
title: '退货政策',
content: '自签收之日起7天内可无理由退货15天内可换货。请保持商品及包装完好。退货运费由买家承担质量问题除外。',
},
'退款': {
title: '退款流程',
content: '退货审核通过后退款将在3-5个工作日内原路返回。如超过时间未到账请联系客服。',
},
'配送': {
title: '配送说明',
content: '默认顺丰快递普通订单1-3天送达偏远地区3-7天。满99元免运费。',
},
'保修': {
title: '保修政策',
content: '电子产品保修期1年自购买之日起计算。人为损坏不在保修范围内。',
},
'会员': {
title: '会员权益',
content: '会员享受9折优惠、免运费、专属客服、生日礼券等权益。年费128元。',
},
};
const results = [];
const q = query || '';
for (const [key, value] of Object.entries(knowledgeBase)) {
if (q.includes(key) || key.includes(q)) {
results.push(value);
}
}
if (results.length === 0) {
results.push({
title: '搜索结果',
content: `未找到与"${query}"直接相关的知识库文档。建议联系人工客服获取更详细的帮助。`,
});
}
return { query, results, total: results.length, source: 'local' };
}
static async getCurrentTime() {
const now = new Date();
return {
datetime: now.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }),
timestamp: now.getTime(),
timezone: 'Asia/Shanghai',
weekday: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][now.getDay()],
};
}
static async calculate({ expression }) {
try {
// 仅允许数字和基本运算符,防止注入
const sanitized = expression.replace(/[^0-9+\-*/().% ]/g, '');
if (!sanitized || sanitized !== expression.replace(/\s/g, '')) {
return { error: '表达式包含不支持的字符', expression };
}
const result = Function('"use strict"; return (' + sanitized + ')')();
return { expression, result: Number(result), formatted: String(result) };
} catch (e) {
return { error: '计算失败: ' + e.message, expression };
}
}
}
module.exports = ToolExecutor;