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

1036 lines
51 KiB
JavaScript
Raw Normal View History

2026-03-12 12:47:56 +08:00
const axios = require('axios');
const https = require('https');
const arkChatService = require('./arkChatService');
const { buildKnowledgeAnswerPrompt, resolveAssistantProfile } = require('./assistantProfileConfig');
const { getAssistantProfile } = require('./assistantProfileService');
const kbRetriever = require('./kbRetriever');
const redisClient = require('./redisClient');
// HTTP keep-alive agent复用TCP连接避免每次请求重新握手
const kbHttpAgent = new https.Agent({
keepAlive: true,
keepAliveMsecs: 30000,
maxSockets: 6,
timeout: 15000,
});
// 连接预热服务启动后自动建立到方舟API的TLS连接避免首次查询的握手延迟
setTimeout(() => {
const warmupKey = process.env.VOLC_ARK_API_KEY || process.env.VOLC_ACCESS_KEY_ID;
if (warmupKey) {
axios.post('https://ark.cn-beijing.volces.com/api/v3/chat/completions', {
model: process.env.VOLC_ARK_KNOWLEDGE_ENDPOINT_ID || process.env.VOLC_ARK_ENDPOINT_ID || 'warmup',
messages: [{ role: 'user', content: 'ping' }],
max_tokens: 1,
stream: false,
}, {
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${warmupKey}` },
timeout: 8000,
httpsAgent: kbHttpAgent,
}).then(() => {
console.log('[ToolExecutor] KB connection pool warmed up');
}).catch(() => {
console.log('[ToolExecutor] KB connection warmup sent (pool established)');
});
}
}, 2000);
const contextKeywordTracker = require('./contextKeywordTracker');
const {
hasCanonicalKnowledgeTerm: hasCanonicalKnowledgeTermMatch,
extractKnowledgeEntityMatches,
hasKeywordFromList,
SYSTEM_ROUTE_KEYWORDS,
COMPANY_ROUTE_KEYWORDS,
PRODUCT_ROUTE_KEYWORDS,
FAQ_ROUTE_KEYWORDS,
SCIENCE_TRAINING_ROUTE_KEYWORDS,
} = require('./knowledgeKeywords');
// KB查询缓存相同effectiveQuery + datasetIds + userId在TTL内直接返回缓存结果
const KB_CACHE_TTL_MS = 5 * 60 * 1000; // 5分钟 (hit结果)
const KB_CACHE_NOHIT_TTL_MS = 2 * 60 * 1000; // 2分钟 (no-hit结果较短TTL)
const KB_CACHE_MAX_SIZE = 200;
const kbQueryCache = new Map();
function getKbCacheKey(query, datasetIds, profileScope = 'global') {
const mode = process.env.VOLC_ARK_KB_RETRIEVAL_MODE || 'answer';
return `vdb2|${mode}|${String(profileScope || 'global').trim() || 'global'}|${(query || '').trim()}|${(datasetIds || []).sort().join(',')}`;
}
function getKbCache(key) {
const entry = kbQueryCache.get(key);
if (!entry) return null;
// hit: 5min TTL; no-hit: 10s 短 TTL仅防同一轮次重复查 VikingDB
const ttl = entry.hit ? KB_CACHE_TTL_MS : 10000;
if (Date.now() - entry.timestamp > ttl) {
kbQueryCache.delete(key);
return null;
}
return entry.result;
}
function setKbCache(key, result) {
if (kbQueryCache.size >= KB_CACHE_MAX_SIZE) {
const oldest = kbQueryCache.keys().next().value;
kbQueryCache.delete(oldest);
}
// hit: 正常缓存; no-hit: 内存 10s 去重(防止同一轮次重复查 VikingDB不写 Redis
kbQueryCache.set(key, { result, timestamp: Date.now(), hit: !!result.hit });
}
2026-03-12 12:47:56 +08:00
class ToolExecutor {
static hasCanonicalKnowledgeTerm(query) {
return hasCanonicalKnowledgeTermMatch(query);
}
static extractKnowledgeEntities(text) {
return extractKnowledgeEntityMatches(text);
}
static classifyQuestionSlot(query) {
const text = String(query || '').trim();
if (/(多少钱|价格|售价|费用|价钱)/.test(text)) return 'price';
if (/(成分|配方|原料|含什么|包含什么)/.test(text)) return 'ingredient';
if (/(规格|包装|剂型|形态|粉末|粉剂|粉状|胶囊|软胶囊|片剂|颗粒|喷雾|乳霜|乳液|凝胶|膏状|口服液|每盒|每袋|每瓶|每支|多少袋|多少粒|多少片|多少毫升|多大规格)/.test(text)) return 'specification';
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事业发展逻辑|商机|PM价值|为什么选择德国PM|为何选择德国PM|为什么选德国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: '用户当前只关心成分或配方,请只回答成分、原料或是否未提及成分,不要扩展到品牌背景。',
specification: '用户当前只关心规格、包装、剂型、形态或每盒每袋等产品细节,请只回答这些明确规格信息;如果知识库没写,就直接说明未提及。',
usage: '用户当前只关心用法、吃法、服用频次或剂量,请只回答这一点。',
side_effect: '用户当前只关心副作用或好转反应,请只回答可能的不良反应、好转反应或注意事项。',
effect_time: '用户当前只关心多久见效或效果周期,请只回答见效时间、周期或个体差异,不要扩展无关信息。',
medical_claim: '用户当前只关心产品能不能治病、是不是药,请只回答是否属于药品、能否替代药物以及相关注意事项。',
bundle_reason: '用户当前只关心为什么要全套、搭配或三合一请只回答搭配原理、协同作用或NTC相关原因。',
business_growth: '用户当前只关心PM事业发展、商机、PM价值、为何选择、线上拓客、陌生客户沟通、一成系统赋能、三大平台四大Ai生态或自我介绍请只回答这类业务发展问题。凡涉及“一部手机做天下、0门槛启动、0成本高效率、足不出户梦想横扫全球、身未动梦已成、批发式晋级”等表述必须明确这是“一成系统”的优势标签是对德国PM事业的软件赋能不要混同为德国PM公司或产品本身。',
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);
// 中英文别名映射改写后的query可能包含英文实体但方舟回答用中文名
const ENTITY_ALIAS_MAP = {
'activize oxyplus': ['小红', 'activize', '艾特维'],
'activize': ['小红', '艾特维'],
'basics': ['大白', '倍适'],
'basic power': ['大白', 'basics'],
'restorate': ['小白', '维适多'],
'fitline': ['pm-fitline', 'pm细胞营养素', '细胞营养素'],
'pm-fitline': ['fitline', '细胞营养素'],
'ntc营养保送系统': ['ntc', '营养保送', '吸收利用'],
'ntc': ['ntc营养保送系统', '营养保送'],
'儿童倍适': ['powercocktail junior', '儿童'],
'cc-cell': ['cc套装', 'cc胶囊', 'cc乳霜'],
'd-drink': ['小绿', '排毒饮', '排毒d饮料'],
'proshape amino': ['氨基酸', 'bcaa'],
'herbal tea': ['草本茶'],
'hair+': ['发宝', '发健'],
'med hair+': ['发宝', '发健'],
'fitness-drink': ['运动饮料', '健康饮品'],
'topshape': ['纤萃', '减肥'],
'generation 50+': ['乐活50+', '乐活'],
'apple antioxy': ['细胞抗氧素', '苹果'],
'zellschutz': ['细胞抗氧素'],
'women+': ['women'],
'men face': ['男士乳霜', '男士护肤'],
'med dental+': ['牙膏', '草本护理'],
'ib5': ['口腔免疫喷雾'],
'q10': ['辅酵素', '氧修护'],
'一成系统': ['三大平台', '四大ai生态', 'ai众享', '数字化工作室', '盛咖学愿'],
};
const expandedEntities = [];
for (const entity of entities) {
const lower = String(entity || '').toLowerCase();
expandedEntities.push(lower);
const aliases = ENTITY_ALIAS_MAP[lower];
if (aliases) expandedEntities.push(...aliases);
}
const mentionsEntity = entities.length === 0 || expandedEntities.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)/,
specification: /(规格|包装|剂型|形态|粉末|粉剂|粉状|胶囊|软胶囊|片剂|颗粒|喷雾|乳霜|乳液|凝胶|膏状|口服液|每盒|每袋|每瓶|每支|袋装|盒装|瓶装|支装|多少袋|多少粒|多少片|多少毫升|克|g|ml)/,
usage: /(服用|用法|用量|每日|每天|一次|次|饭前|饭后|早餐|晚餐|早晚|空腹|睡前)/,
side_effect: /(副作用|不良反应|好转反应|排毒|整应|皮肤.*痒|排便|反应|注意事项|正常现象)/,
effect_time: /(见效|有效|几天|几周|几个月|周期|坚持|因人而异|吸收利用)/,
medical_claim: /(不是药|不能替代药|不能代替药物|不是用于治疗|不能治疗|保健食品|营养补充|就医|医生)/,
bundle_reason: /(全套|搭配|协同|三合一|组合|NTC|吸收|运输|利用|代谢|原理)/,
business_growth: /(一成系统|PM事业|商机|价值|选择|拓客|成交|邀约|陌生客户|沟通|三大平台|四大Ai生态|数字化工作室|Ai众享|盛咖学愿|故事|自我介绍|赋能|智能生产力|软件赋能|一部手机|0门槛|零门槛|0成本|零成本|身未动梦已成|批发式晋级)/,
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 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}`.toLowerCase();
// 5路意图检测system > company > faq > science > product
const hasSystemIntent = hasKeywordFromList(haystack, SYSTEM_ROUTE_KEYWORDS);
const hasCompanyIntent = hasKeywordFromList(haystack, COMPANY_ROUTE_KEYWORDS);
const hasProductIntent = hasKeywordFromList(haystack, PRODUCT_ROUTE_KEYWORDS);
const hasFaqIntent = hasKeywordFromList(haystack, FAQ_ROUTE_KEYWORDS);
const hasScienceIntent = hasKeywordFromList(haystack, SCIENCE_TRAINING_ROUTE_KEYWORDS);
// 确定路由:多意图可并行,只排除真正冲突的组合
const priorityRouteNames = [];
if (hasSystemIntent) priorityRouteNames.push('system');
if (hasProductIntent) {
priorityRouteNames.push('product');
// 产品问题同时搜FAQ和科普获取更全面的回答好转反应、科普误区等补充信息
if (!hasFaqIntent) priorityRouteNames.push('faq');
if (!hasScienceIntent) priorityRouteNames.push('science');
}
if (hasCompanyIntent) {
priorityRouteNames.push('company');
// 公司问题同时搜产品和系统培训test collection 内容有限
if (!hasProductIntent) priorityRouteNames.push('product');
if (!hasSystemIntent) priorityRouteNames.push('system');
}
if (hasFaqIntent) priorityRouteNames.push('faq');
if (hasScienceIntent) priorityRouteNames.push('science');
if (priorityRouteNames.length > 0) {
const routingRules = this.getKnowledgeBaseRoutingRules();
const priorityRules = routingRules.filter((rule) => priorityRouteNames.includes(rule.name));
const priorityDatasetIds = [...new Set(priorityRules.flatMap((rule) => rule.dataset_ids).filter(Boolean))];
if (priorityDatasetIds.length > 0) {
console.log(`[ToolExecutor] KB 5-way route: intents=[${priorityRouteNames.join(',')}] datasets=[${priorityDatasetIds.join(',')}]`);
return {
datasetIds: priorityDatasetIds,
matchedRoutes: [...new Set(priorityRouteNames)],
};
}
}
// 通用env规则匹配回退
const matchedDatasetIds = [];
const matchedRoutes = [];
for (const rule of this.getKnowledgeBaseRoutingRules()) {
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();
// ====================================================================
// 精简版:只保留 VikingDB 语义检索已知会失败的场景
// 产品/公司/认证等查询全部交给 VikingDB + reranker 处理原始语义
// 追问/代词由 enrichQueryWithContext + KB保护窗口 处理
// ====================================================================
// === 一成系统子话题分流(内部术语,向量检索难区分子话题) ===
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 (/(一部手机|0门槛|零门槛|0成本|零成本|足不出户|梦想横扫全球|一部手机做天下)/i.test(text)) return '一成系统 软件赋能 0成本高效率 一部手机做天下 足不出户梦想横扫全球';
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 (/(一部手机做天下|一部手机即可运营全球市场|0门槛启动|零门槛启动|0成本高效率|零成本高效率|足不出户梦想横扫全球|身未动,?梦已成|批发式晋级)/i.test(text)) {
return '一成系统 软件赋能 德国PM事业 0成本高效率 一部手机做天下 身未动梦已成 批发式晋级';
}
if (/(身未动,?梦已成|批发式晋级)/i.test(text)) return '一成系统 身未动梦已成 批发式晋级 三大平台 四大Ai生态';
if (/行动圈/i.test(text)) return '一成系统 行动圈 数字化工作室 团队管理 目标考核';
if (/盟主社区/i.test(text)) return '一成系统 盟主社区 AI众享 社区盟主 引流 转化';
// === 一成系统相关业务话题 ===
if (/(招商|代理|加盟|事业机会|招商稿|代理政策)/i.test(text)) return '一成系统 PM事业 招商与代理 软件赋能 0成本高效率';
if (/(如何发展PM事业|怎么发展PM事业|PM事业发展逻辑|介绍PM事业|两分钟介绍PM事业)/i.test(text)) return '一成系统 PM事业 发展逻辑 商机 价值 软件赋能 三大平台 四大Ai生态 0成本高效率';
if (/(为什么选择德国PM|为何选择德国PM|为什么选德国PM|为什么选PM|为何选PM)/i.test(text)) return '一成系统 德国PM 选择理由 公司实力 产品优势 软件赋能 0成本高效率';
if (/(陌生客户|陌生人).*(沟通|开口|邀约|交流|切入).*(PM事业|德国PM|PM)/i.test(text)) return '一成系统 PM事业 陌生客户 沟通 邀约 话术 软件赋能';
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 '火炉原理';
// 所有其它查询(产品/公司/认证/培训等):不做确定性改写
// 依赖 normalizeKnowledgeQueryAlias别名归一化+ enrichQueryWithContext上下文补充+ VikingDB + reranker
return null;
}
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(/^(你|你们|帮我|麻烦你|请你?|我想|我要|能不能|可以|可不可以|能否)[给帮]?(我)?(查一下|查查|查下|搜一下|搜搜|搜下|找一下|找找|找下|看一下|看看|看下|说一下|说说|说下|讲一下|讲讲|讲下|介绍一下|介绍下)?/g, '')
.replace(/(的)?(相关|详细)?(内容|信息|资料|介绍|说明)[。??]*$/g, '')
.replace(/一成[,、。!?\s]+系统/g, '一成系统')
.replace(/X{2}系统/gi, '一成系统')
.replace(/[\u4e00-\u9fff]{1,3}(?:成|城|程|诚|乘|声|生)[,、\s]*系统/g, '一成系统')
.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, 'Basics 活力健')
.replace(/基础三合一|三合一基础套|大白小红小白|基础套装?/g, 'PM细胞营养素 基础套装')
.replace(/儿童倍适|儿童产品/g, '儿童倍适')
.replace(/小红精华液/g, 'Activize Serum 小红精华液')
.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, '阿育吠陀')
.replace(/好转反应|整健反应|调整反应/g, '好转反应 整健反应')
.replace(/舒采健|Women\+/gi, 'Women+ 舒采健')
.replace(/骨骼健/g, '骨骼健 关节套装')
.replace(/顾心/g, '顾心 心脏保护')
.replace(/衡醇饮|小粉C|小粉(?!红)/g, '衡醇饮 小粉C')
.replace(/异黄酮/g, '异黄酮素 Isoflavon')
.replace(/(?<!儿童)倍适(?!多)/g, 'PowerCocktail 倍适')
.replace(/PowerCocktail\s*Junior/gi, 'PowerCocktail Junior 儿童倍适')
.replace(/(?<!Junior )(?<!倍适 )PowerCocktail/gi, 'PowerCocktail 倍适')
.replace(/苹果细胞抗氧素|苹果抗氧素/g, 'Apple Antioxy Zellschutz 苹果细胞抗氧素')
.replace(/(?:全效)?眼霜/g, 'Eye Cream 全效眼霜')
.replace(/(?:洁面乳|洗面奶|洁面)/g, 'Cleansing Lotion 洁面乳')
.replace(/爽肤水/g, 'Tonic 爽肤水')
.replace(/蛋白粉|餐代餐|代餐奶昔/g, 'ProShape 全效纤体营养餐代餐')
// === 产品俗名/简称 → 标准名+英文名(增强向量检索命中率)===
.replace(/小绿/g, 'D-Drink 小绿 排毒饮')
.replace(/(?<!小绿 )排毒饮/g, 'D-Drink 排毒饮')
.replace(/(?<!草本护理)牙膏/g, '草本护理牙膏 Med Dental+')
.replace(/(?:口腔免疫喷雾|口腔喷雾|免疫喷雾)/g, 'IB5 口腔免疫喷雾')
.replace(/(?<!免疫)喷雾/g, 'IB5 口腔免疫喷雾')
.replace(/(?<!Herbal Tea )草本茶/g, 'Herbal Tea 草本茶')
.replace(/发宝|发健/g, 'Med Hair+ 发宝')
.replace(/(?:男士乳霜|男士护肤|男士面霜)/g, 'Men Face 男士护肤乳霜')
.replace(/纤萃/g, 'TopShape 纤萃')
.replace(/运动饮料/g, 'Fitness-Drink 运动饮料')
.replace(/(?<!Generation 50\+? )乐活/g, 'Generation 50+ 乐活')
.replace(/(?<!Zellschutz )细胞抗氧素/g, 'Zellschutz 细胞抗氧素')
.replace(/CC套装|CC胶囊|CC乳霜/g, 'CC-Cell')
.replace(/(?<!Q10 )辅酵素/g, 'Q10 辅酵素')
.replace(/氧修护/g, 'Q10 氧修护')
.replace(/小黑/g, 'MEN+ 倍力健 小黑')
.replace(/(?<!MEN\+? )倍力健/g, 'MEN+ 倍力健')
.replace(/(?<!ProShape Amino )氨基酸/g, 'ProShape Amino 氨基酸')
.replace(/BCAA/gi, 'ProShape Amino BCAA')
.replace(/(?<!胶原蛋白)胶原蛋白(?!肽)/g, '胶原蛋白肽')
.replace(/乳酪煲|乳酪饮品|乳酪/g, '乳酪煲 乳酪饮品')
.replace(/(?<!关节套装 )关节舒缓/g, '关节套装 关节舒缓')
.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)) {
// 长度兜底:回答内容足够长(>=60字且不含无结果模式时倾向判定为hit
// 这避免了方舟LLM用同义词表达导致slot正则不匹配的误杀
if (text.length >= 60 && !strictNoHitPattern.test(text)) {
console.log(`[ToolExecutor] slot_mismatch overridden by length fallback: query="${query}" len=${text.length}`);
return {
hit: true,
reason: 'length_fallback',
reply: text,
};
}
return {
hit: false,
reason: 'slot_mismatch',
reply: `知识库中暂未找到与"${query}"直接相关的信息,请换个更具体的问法再试。`,
};
}
return {
hit: true,
reason: 'hit',
reply: text,
};
}
2026-03-12 12:47:56 +08:00
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 = 'answer', context = [], session_id = null, original_text = '', _session = null, skipCache = false }) {
2026-03-12 12:47:56 +08:00
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;
const profileUserId = _session?.profileUserId || _session?.userId || null;
const assistantProfileResult = await getAssistantProfile({ userId: profileUserId });
const assistantProfile = resolveAssistantProfile({
...(assistantProfileResult?.profile || {}),
...(_session?.assistantProfile || {}),
});
if (_session && assistantProfileResult?.profile) {
_session.assistantProfile = assistantProfile;
}
const profileScope = profileUserId || 'global';
2026-03-12 12:47:56 +08:00
console.log(`[ToolExecutor] searchKnowledge called with query="${query}"`);
if (!knowledgeEndpointId) {
console.warn('[ToolExecutor] searchKnowledge skipped: knowledge endpoint not configured');
return {
query,
original_query: query,
rewritten_query: query,
results: [{ title: '配置缺失', content: `知识库中暂未找到与"${query}"直接相关的信息,请换个更具体的问法再试。` }],
total: 1,
source: 'ark_knowledge',
hit: false,
reason: 'endpoint_not_configured',
};
}
const rewrittenQuery = this.rewriteKnowledgeQuery(query, context, session_id, _session);
// 全库检索:始终搜索所有 collection由 VikingDB + reranker 判断相关性
const allDatasetIds = String(process.env.VOLC_ARK_KNOWLEDGE_BASE_IDS || '')
.split(',').map(id => id.trim()).filter(Boolean);
const kbTarget = { datasetIds: allDatasetIds, matchedRoutes: ['all'] };
const effectiveQuery = rewrittenQuery || query;
if (rewrittenQuery && rewrittenQuery !== query) {
console.log(`[ToolExecutor] searchKnowledge rewritten query="${rewrittenQuery}"`);
}
console.log(`[ToolExecutor] searchKnowledge full-scan all ${allDatasetIds.length} collections`);
2026-03-12 12:47:56 +08:00
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',
};
}
2026-03-12 12:47:56 +08:00
try {
// 缓存检查:优先 Redis降级内存 MapskipCache 时跳过)
const cacheKey = getKbCacheKey(effectiveQuery, kbTarget.datasetIds, profileScope);
const redisCached = skipCache ? null : await redisClient.getKbCache(cacheKey).catch(() => null);
const cached = skipCache ? null : (redisCached || getKbCache(cacheKey));
if (cached) {
const latencyMs = Date.now() - startTime;
console.log(`[ToolExecutor] Ark KB cache hit in ${latencyMs}ms key="${cacheKey.slice(0, 60)}" source=${redisCached ? 'redis' : 'memory'}`);
return {
...cached,
original_query: query,
rewritten_query: effectiveQuery,
selected_dataset_ids: kbTarget.datasetIds,
selected_kb_routes: kbTarget.matchedRoutes,
latency_ms: latencyMs,
cache_hit: true,
};
}
// 根据检索模式选择链路
const retrievalMode = process.env.VOLC_ARK_KB_RETRIEVAL_MODE || 'answer';
let arkResult;
if (retrievalMode === 'raw') {
// ★ 新链路:纯检索 + 重排,不经 LLM 加工
console.log('[ToolExecutor] Using RAW retrieval mode (kbRetriever)');
const rawResult = await kbRetriever.searchAndRerank(effectiveQuery, {
datasetIds: kbTarget.datasetIds,
sessionId: session_id,
session: _session,
originalQuery: query,
});
// 转换为与旧格式兼容的结构
arkResult = {
query: rawResult.query,
results: rawResult.ragPayload.length > 0
? rawResult.ragPayload.map(item => ({ title: item.title, content: item.content }))
: [{ title: '未找到', content: `知识库中暂未找到与"${query}"直接相关的信息,请换个更具体的问法再试。` }],
total: rawResult.ragPayload.length,
source: 'ark_knowledge',
hit: rawResult.hit,
reason: rawResult.reason,
retrieval_mode: 'raw',
top_score: rawResult.topScore,
chunks_count: rawResult.rerankedChunks?.length || 0,
};
} else {
// 旧链路LLM 加工模式
console.log('[ToolExecutor] Using ANSWER retrieval mode (searchArkKnowledge)');
arkResult = await this.searchArkKnowledge(effectiveQuery, context, responseMode, kbTarget.datasetIds, query, assistantProfile);
}
const latencyMs = Date.now() - startTime;
console.log(`[ToolExecutor] Ark KB search succeeded in ${latencyMs}ms mode=${retrievalMode}`);
// 缓存到 Redis + 内存双写
setKbCache(cacheKey, arkResult);
redisClient.setKbCache(cacheKey, arkResult).catch(() => {});
return {
...arkResult,
original_query: query,
rewritten_query: effectiveQuery,
selected_dataset_ids: kbTarget.datasetIds,
selected_kb_routes: kbTarget.matchedRoutes,
latency_ms: latencyMs,
};
2026-03-12 12:47:56 +08:00
} catch (error) {
const latencyMs = Date.now() - startTime;
2026-03-12 12:47:56 +08:00
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',
};
2026-03-12 12:47:56 +08:00
}
}
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, session = null) {
const originalQuery = String(query || '').trim();
if (!originalQuery) {
return '';
}
// 先做别名归一化ASR变体如"移程系统"→"一成系统"),再尝试确定性改写
const aliasNormalized = this.normalizeKnowledgeQueryAlias(originalQuery);
const deterministicQuery = this.buildDeterministicKnowledgeQuery(aliasNormalized, context);
if (deterministicQuery) {
console.log(`[ToolExecutor] deterministic rewrite: "${originalQuery}" → "${deterministicQuery}"`);
return deterministicQuery;
}
let normalizedQuery = this.applyKnowledgeQueryAnchor(aliasNormalized);
if (sessionId) {
normalizedQuery = contextKeywordTracker.enrichQueryWithContext(sessionId, normalizedQuery, session);
}
return this.sanitizeRewrittenQuery(normalizedQuery);
}
static sanitizeRewrittenQuery(query) {
let cleaned = String(query || '').trim();
if (!cleaned) return cleaned;
// 1. 清理口语填充词/语气词
cleaned = cleaned.replace(/[啊哦嗯呢呀哎诶额嘛吧啦哇噢]+/g, ' ');
// 2. 清理连续标点
cleaned = cleaned.replace(/[,。!?!?\s]{2,}/g, ' ');
// 3. 去除重复的问句片段(如"怎么吃 怎么吃"
cleaned = cleaned.replace(/(.{3,}?)[?!。,,\s]+\1/g, '$1');
// 4. 按空格分词去重(保序)
const parts = cleaned.split(/\s+/).filter(Boolean);
const seen = new Set();
const deduped = parts.filter(p => {
if (seen.has(p)) return false;
seen.add(p);
return true;
});
cleaned = deduped.join(' ').trim();
// 5. 截断最大80字符避免过长query降低KB检索质量
if (cleaned.length > 80) {
cleaned = cleaned.slice(0, 80).replace(/\s\S*$/, '').trim();
console.log(`[ToolExecutor] query truncated to 80 chars: "${cleaned}"`);
}
return cleaned;
2026-03-12 12:47:56 +08:00
}
/**
* 通过方舟 Chat Completions API + knowledge_base metadata 进行知识检索
* 使用独立的 LLM 调用专门用于知识库检索场景如语音通话的工具回调
*/
static async searchArkKnowledge(query, context = [], responseMode = 'answer', datasetIdsOverride = null, originalQuery = null, assistantProfile = null) {
const endpointId = process.env.VOLC_ARK_KNOWLEDGE_ENDPOINT_ID || process.env.VOLC_ARK_ENDPOINT_ID;
const kbModel = process.env.VOLC_ARK_KB_MODEL || endpointId;
2026-03-12 12:47:56 +08:00
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);
2026-03-12 12:47:56 +08:00
const topK = parseInt(process.env.VOLC_ARK_KNOWLEDGE_TOP_K) || 3;
const threshold = parseFloat(process.env.VOLC_ARK_KNOWLEDGE_THRESHOLD) || 0.3;
2026-03-12 12:47:56 +08:00
// 当 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}")`);
}
// 提取最近2轮对话作为上下文最多4条user/assistant消息减少token量加速生成
2026-03-12 12:47:56 +08:00
const recentContext = context
.filter(m => m.role === 'user' || m.role === 'assistant')
.slice(responseMode === 'snippet' ? -2 : -4);
2026-03-12 12:47:56 +08:00
const baseSnippetPrompt = '知识库片段提取助手。提取2-4条与问题最相关的简洁事实片段。只输出中文事实不寒暄不写"根据知识库",不补充未出现的内容,无相关内容则说未找到。';
const baseAnswerPrompt = buildKnowledgeAnswerPrompt(assistantProfile);
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}",请围绕这个问题回答,不要偏离用户的真实意图。下方的检索词仅用于匹配知识库文档,不代表用户的真正提问。`;
}
2026-03-12 12:47:56 +08:00
const messages = [
{
role: 'system',
content: systemContent,
2026-03-12 12:47:56 +08:00
},
...recentContext,
{
role: 'user',
content: effectiveQuery,
},
];
if (recentContext.length > 0) {
console.log(`[ToolExecutor] Ark KB search with ${recentContext.length} context messages`);
}
const body = {
model: kbModel,
2026-03-12 12:47:56 +08:00
messages,
metadata: {
knowledge_base: {
dataset_ids: datasetIds,
top_k: topK,
threshold: threshold,
},
},
stream: false,
max_tokens: 80,
thinking: { type: 'disabled' },
2026-03-12 12:47:56 +08:00
};
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: 15000,
httpsAgent: kbHttpAgent,
2026-03-12 12:47:56 +08:00
}
);
const content = response.data?.choices?.[0]?.message?.content || '未找到相关信息';
const classifyQuery = [effectiveQuery, (originalQuery || '').trim()].filter(Boolean).join(' ');
const classified = this.classifyKnowledgeAnswer(classifyQuery, content);
2026-03-12 12:47:56 +08:00
return {
query,
results: [{
title: '方舟知识库检索结果',
content: classified.reply,
2026-03-12 12:47:56 +08:00
}],
total: 1,
source: 'ark_knowledge',
hit: classified.hit,
reason: classified.reason,
2026-03-12 12:47:56 +08:00
};
}
/**
* 通过 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 });
2026-03-12 12:47:56 +08:00
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;