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

459 lines
19 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');
class ToolExecutor {
static hasCanonicalKnowledgeTerm(query) {
return /(一成系统|PM-FitLine|PM细胞营养素|NTC营养保送系统|Activize Oxyplus|小红产品|Basics|大白产品|Restorate|小白产品|儿童倍适|火炉原理|阿育吠陀)/i.test(String(query || ''));
}
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营养保送系统|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, '小红产品 Activize Oxyplus')
.replace(/大白产品|大白/g, '大白产品 Basics')
.replace(/小白产品|小白/g, '小白产品 Restorate')
.replace(/儿童倍适|儿童产品/g, '儿童倍适')
.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 noHitPattern = /未检索到|没有检索到|没有相关内容|暂无相关内容|未找到相关信息|没有找到相关信息|知识库中没有相关内容|暂未找到与.*直接相关的信息|无法基于知识库/;
if (noHitPattern.test(text)) {
return {
hit: false,
reason: 'no_hit',
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 } = {}, context = []) {
const startTime = Date.now();
query = query || '';
const responseMode = response_mode === 'snippet' ? 'snippet' : 'answer';
console.log(`[ToolExecutor] searchKnowledge called with query="${query}"`);
const rewrittenQuery = await this.rewriteKnowledgeQuery(query, context);
if (rewrittenQuery && rewrittenQuery !== query) {
console.log(`[ToolExecutor] searchKnowledge rewritten query="${rewrittenQuery}"`);
}
const kbIds = process.env.VOLC_ARK_KNOWLEDGE_BASE_IDS;
if (kbIds && kbIds !== 'your_knowledge_base_dataset_id') {
try {
console.log('[ToolExecutor] Trying Ark Knowledge Search...');
const result = await this.searchArkKnowledge(rewrittenQuery || query, context, responseMode);
const latencyMs = Date.now() - startTime;
console.log(`[ToolExecutor] Ark KB search succeeded in ${latencyMs}ms`);
return {
...result,
original_query: query,
rewritten_query: rewrittenQuery || query,
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: rewrittenQuery || query,
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: rewrittenQuery || query,
latency_ms: latencyMs,
errorType: 'not_configured',
error: '知识库未配置,请检查 VOLC_ARK_KNOWLEDGE_BASE_IDS',
source: 'ark_knowledge',
hit: false,
reason: 'not_configured',
};
}
static async rewriteKnowledgeQuery(query, context = []) {
const originalQuery = String(query || '').trim();
if (!originalQuery) {
return '';
}
const normalizedQuery = this.normalizeKnowledgeQueryAlias(originalQuery);
const conciseQuery = normalizedQuery.replace(/[,。!?、,.!?\s]+/g, '');
const recentContext = (Array.isArray(context) ? context : [])
.filter((item) => item && (item.role === 'user' || item.role === 'assistant') && String(item.content || '').trim())
.slice(-6)
.map((item) => `${item.role === 'user' ? '用户' : '助手'}${String(item.content || '').trim()}`)
.join('\n');
const isPronounFollowUp = /^(这个|那个|它|该系统|这个系统|那个系统|详细|继续|怎么|为什么|适合谁|什么意思)/.test(normalizedQuery);
if (this.hasCanonicalKnowledgeTerm(normalizedQuery) && conciseQuery.length <= 36 && !isPronounFollowUp) {
return normalizedQuery;
}
if (!process.env.VOLC_ARK_ENDPOINT_ID || process.env.VOLC_ARK_ENDPOINT_ID === 'your_ark_endpoint_id') {
return normalizedQuery;
}
try {
const result = await arkChatService.chat([
{
role: 'system',
content: '你是知识库检索词改写助手。你的任务是把用户当前问题改写成适合企业知识库检索的完整查询语句。必须处理三类问题1补全多轮对话中的省略主语2纠正语音识别错误、口语噪声和同音误写3把别名统一成知识库里的规范说法。规则不要改变用户真实意图不要回答问题只输出一行最终检索词优先保留真正的产品名、系统名、技术名。当前知识库高频规范术语包括一成系统、PM-FitLine、PM细胞营养素、NTC营养保送系统、Activize Oxyplus、小红产品、Basics、大白产品、Restorate、小白产品、儿童倍适、火炉原理、阿育吠陀。示例XX系统、一城系统、逸城系统、一程系统等都统一理解为一成系统NTC营养配送系统、NTC营养输送系统统一为NTC营养保送系统Fitline、PM fitline 统一为 PM-FitLine小红统一为小红产品 Activize Oxyplus。',
},
{
role: 'user',
content: `最近上下文:\n${recentContext || '无'}\n\n当前原始问题:${normalizedQuery}\n\n请输出最终检索词:`,
},
], []);
const rewritten = this.normalizeKnowledgeQueryAlias(String(result.content || '').replace(/^["'“”]+|["'“”]+$/g, '').trim());
return rewritten || normalizedQuery;
} catch (error) {
console.warn('[ToolExecutor] rewriteKnowledgeQuery failed:', error.message);
return normalizedQuery;
}
}
/**
* 通过方舟 Chat Completions API + knowledge_base metadata 进行知识检索
* 使用独立的 LLM 调用,专门用于知识库检索场景(如语音通话的工具回调)
*/
static async searchArkKnowledge(query, context = [], responseMode = 'answer') {
const endpointId = 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;
const datasetIds = 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.5;
// 当 query 为空时FC 流式 chunks 乱序无法解析),使用简短的默认查询
const effectiveQuery = (query && query.trim()) ? query : '请介绍你们的产品和服务';
if (!query || !query.trim()) {
console.log('[ToolExecutor] Empty query, using default: "' + effectiveQuery + '"');
}
// 提取最近 3 轮对话作为上下文(最多 6 条 user/assistant 消息)
const recentContext = context
.filter(m => m.role === 'user' || m.role === 'assistant')
.slice(responseMode === 'snippet' ? -4 : -6);
const messages = [
{
role: 'system',
content: responseMode === 'snippet'
? '你是知识库片段提取助手。请基于知识库提取与用户问题最相关的2到4条简洁知识片段供语音系统继续组织回复。规则只输出直接相关的中文事实片段每条尽量简短不要寒暄不要解释你的任务不要写“根据知识库”如果没有相关内容请明确说未找到相关内容。'
: '你是一个知识库检索助手。请根据知识库中的内容回答用户问题。如果知识库中没有相关内容,请如实说明。回答时请引用知识库来源。',
},
...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 classified = this.classifyKnowledgeAnswer(query, 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;