596 lines
21 KiB
JavaScript
596 lines
21 KiB
JavaScript
import axios from 'axios';
|
||
import { executeWithRetry } from '../hooks/useWeCom';
|
||
|
||
// API 基础配置
|
||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://mp.yizhezhe.com/api';
|
||
const CUSTOMER_API_BASE = import.meta.env.VITE_CUSTOMER_API_BASE || 'https://admin.doouba.cn/api';
|
||
|
||
// 创建 axios 实例
|
||
const apiClient = axios.create({
|
||
baseURL: API_BASE_URL,
|
||
timeout: 10000,
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
const customerApiClient = axios.create({
|
||
baseURL: CUSTOMER_API_BASE,
|
||
timeout: 10000,
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
/**
|
||
* 认证服务
|
||
*/
|
||
export const authService = {
|
||
/**
|
||
* 获取 Access Token
|
||
* @returns {Promise} Access Token 数据
|
||
*/
|
||
getAccessToken: () => executeWithRetry(async () => {
|
||
console.log('获取 Access Token...');
|
||
const response = await apiClient.get('/auth/token');
|
||
|
||
if (response.data.success !== true) {
|
||
throw new Error(`获取 Access Token 失败: ${response.data.message}`);
|
||
}
|
||
|
||
console.log('Access Token 获取成功:', response.data);
|
||
return response.data;
|
||
}, 3, (retryCount) => {
|
||
console.log(`第${retryCount}次重试获取 Access Token...`);
|
||
}),
|
||
|
||
/**
|
||
* 获取 JSAPI Ticket
|
||
* @returns {Promise} JSAPI Ticket 数据
|
||
*/
|
||
getJsapiTicket: () => executeWithRetry(async () => {
|
||
console.log('获取 JSAPI Ticket...');
|
||
const response = await apiClient.get('/auth/ticket');
|
||
|
||
if (response.data.success !== true) {
|
||
throw new Error(`获取 JSAPI Ticket 失败: ${response.data.message}`);
|
||
}
|
||
|
||
console.log('JSAPI Ticket 获取成功:', response.data);
|
||
return response.data;
|
||
}, 3, (retryCount) => {
|
||
console.log(`第${retryCount}次重试获取 JSAPI Ticket...`);
|
||
})
|
||
};
|
||
|
||
/**
|
||
* 客户服务
|
||
*/
|
||
export const customerService = {
|
||
/**
|
||
* 获取客户标签属性
|
||
* @param {string} userId 客户ID
|
||
* @returns {Promise} 客户信息(包含标签)
|
||
*/
|
||
getCustomerTags: (userId) => executeWithRetry(async () => {
|
||
console.log(`获取客户 ${userId} 的标签属性...`);
|
||
|
||
const response = await customerApiClient.get(`/WechatCustomers/${userId}`, {
|
||
params: {
|
||
includeWechatInfo: true
|
||
}
|
||
});
|
||
|
||
if (response.data.code !== 200) {
|
||
throw new Error(`获取客户标签失败: ${response.data.msg}`);
|
||
}
|
||
|
||
console.log('客户标签获取成功:', response.data.data);
|
||
return response.data.data;
|
||
}, 3, (retryCount) => {
|
||
console.log(`第${retryCount}次重试获取客户标签...`);
|
||
}),
|
||
|
||
/**
|
||
* 根据标签和历史会话生成提示词
|
||
* @param {Object} customerInfo 客户信息
|
||
* @param {Array} messageHistory 消息历史数据
|
||
* @returns {string} 生成的提示词
|
||
*/
|
||
generatePromptFromTagsAndHistory: (customerInfo, messageHistory = []) => {
|
||
if (!customerInfo || !customerInfo.tags) {
|
||
return '你好!我是兜爸心选的客服,有什么可以帮您的吗?';
|
||
}
|
||
|
||
const { name, tags, externalUserId } = customerInfo;
|
||
const tagNames = tags.map(t => t.TagName).join('、');
|
||
const tagCategories = [...new Set(tags.map(t => t.TagCategory))].join('、');
|
||
|
||
// 1. 基础客户信息
|
||
let prompt = `【客户基本信息】\n`;
|
||
prompt += `- 姓名:${name || '未知客户'}\n`;
|
||
|
||
if (tagNames) {
|
||
prompt += `- 标签:${tagNames}\n`;
|
||
}
|
||
|
||
if (tagCategories) {
|
||
prompt += `- 标签分类:${tagCategories}\n`;
|
||
}
|
||
|
||
// 添加基于标签的个性化内容
|
||
const hasXiaohongshu = tags.some(t => t.TagName.includes('小红书'));
|
||
const hasYunying = tags.some(t => t.TagCategory === '运营人');
|
||
|
||
if (hasXiaohongshu) {
|
||
prompt += `- 渠道:小红书用户,对性价比比较关注\n`;
|
||
}
|
||
|
||
if (hasYunying) {
|
||
const yunyingTags = tags.filter(t => t.TagCategory === '运营人');
|
||
if (yunyingTags.length > 0) {
|
||
prompt += `- 运营人:${yunyingTags[0].TagName}\n`;
|
||
}
|
||
}
|
||
|
||
// 2. 历史会话摘要
|
||
prompt += `\n【历史会话摘要】(最近${messageHistory.length}条)\n`;
|
||
|
||
if (messageHistory.length > 0) {
|
||
// 按时间倒序排列(最新的在前)
|
||
const sortedHistory = [...messageHistory].sort((a, b) =>
|
||
new Date(b.msgtime) - new Date(a.msgtime)
|
||
).slice(0, 20); // 确保最多20条
|
||
|
||
sortedHistory.forEach((msg, index) => {
|
||
const time = new Date(msg.msgtime).toLocaleString('zh-CN');
|
||
const role = msg.from_user === externalUserId ? '客户' : '客服';
|
||
const content = msg.text?.content || msg.content || '无文本内容';
|
||
|
||
// 简化内容,避免过长
|
||
const simplifiedContent = content.length > 50
|
||
? content.substring(0, 50) + '...'
|
||
: content;
|
||
|
||
prompt += `${index + 1}. 时间:${time},角色:${role},内容:${simplifiedContent}\n`;
|
||
});
|
||
|
||
// 3. 对话上下文分析
|
||
prompt += `\n【对话上下文分析】\n`;
|
||
|
||
// 分析客户消息
|
||
const customerMessages = sortedHistory.filter(msg =>
|
||
msg.from_user === externalUserId
|
||
);
|
||
|
||
if (customerMessages.length > 0) {
|
||
// 提取关键词
|
||
const allContent = customerMessages.map(msg =>
|
||
msg.text?.content || msg.content || ''
|
||
).join(' ');
|
||
|
||
// 简单分析(实际可以更复杂)
|
||
prompt += `- 客户主要关注:${customerService.analyzeMainTopics(allContent)}\n`;
|
||
prompt += `- 消息频率:${customerMessages.length}条历史消息\n`;
|
||
|
||
// 检查是否有未解决的问题
|
||
const hasQuestions = customerMessages.some(msg =>
|
||
(msg.text?.content || '').includes('?') ||
|
||
(msg.text?.content || '').includes('?')
|
||
);
|
||
prompt += `- 未解决问题:${hasQuestions ? '有' : '暂无明确未解决问题'}\n`;
|
||
}
|
||
} else {
|
||
prompt += `暂无历史会话记录\n`;
|
||
}
|
||
|
||
// 4. 生成要求
|
||
prompt += `\n【生成要求】\n`;
|
||
prompt += `基于以上客户信息和历史会话,生成一段亲切、专业的回复。要求:\n`;
|
||
prompt += `1. 如果历史会话中有未解决的问题,优先回应\n`;
|
||
prompt += `2. 保持对话的连贯性和上下文一致性\n`;
|
||
prompt += `3. 体现对客户历史需求的了解和关注\n`;
|
||
prompt += `4. 根据客户标签提供个性化服务建议\n`;
|
||
prompt += `5. 语气亲切但不失专业,体现兜爸心选的品牌形象\n`;
|
||
|
||
return prompt;
|
||
},
|
||
|
||
/**
|
||
* 简单的话题分析函数
|
||
* @param {string} text 文本内容
|
||
* @returns {string} 分析出的主要话题
|
||
*/
|
||
analyzeMainTopics: (text) => {
|
||
const topics = [];
|
||
const lowerText = text.toLowerCase();
|
||
|
||
if (lowerText.includes('价格') || lowerText.includes('多少钱')) {
|
||
topics.push('价格咨询');
|
||
}
|
||
if (lowerText.includes('怎么用') || lowerText.includes('使用方法')) {
|
||
topics.push('使用指导');
|
||
}
|
||
if (lowerText.includes('发货') || lowerText.includes('物流')) {
|
||
topics.push('物流查询');
|
||
}
|
||
if (lowerText.includes('质量') || lowerText.includes('品质')) {
|
||
topics.push('质量咨询');
|
||
}
|
||
if (lowerText.includes('售后') || lowerText.includes('退换')) {
|
||
topics.push('售后服务');
|
||
}
|
||
|
||
return topics.length > 0 ? topics.join('、') : '一般咨询';
|
||
},
|
||
|
||
/**
|
||
* 格式化历史会话用于显示
|
||
* @param {Array} messageHistory 消息历史数据
|
||
* @param {string} externalUserId 客户ID(用于识别客户消息)
|
||
* @param {number} maxMessages 最大显示消息数
|
||
* @returns {string} 格式化后的历史会话文本
|
||
*/
|
||
formatHistoryForDisplay: (messageHistory = [], externalUserId = '', maxMessages = 5) => {
|
||
if (!messageHistory || messageHistory.length === 0) {
|
||
return '📜 暂无历史会话记录';
|
||
}
|
||
|
||
// 按时间倒序排列(最新的在前)
|
||
const sortedHistory = [...messageHistory].sort((a, b) =>
|
||
new Date(b.msgtime) - new Date(a.msgtime)
|
||
).slice(0, maxMessages);
|
||
|
||
let formattedText = `📜 最近对话记录(${sortedHistory.length}条):\n\n`;
|
||
|
||
sortedHistory.forEach((msg) => {
|
||
// 确定发送者角色
|
||
const role = msg.from_user === externalUserId ? '客户' : '客服';
|
||
|
||
// 获取消息内容
|
||
const content = msg.text?.content || msg.content || '无文本内容';
|
||
|
||
// 简化内容,避免过长
|
||
const simplifiedContent = content.length > 80
|
||
? content.substring(0, 80) + '...'
|
||
: content;
|
||
|
||
// 使用点状列表格式,弱化时间显示
|
||
formattedText += `• ${role}:${simplifiedContent}\n`;
|
||
});
|
||
|
||
return formattedText;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 消息服务
|
||
*/
|
||
export const messageService = {
|
||
/**
|
||
* 获取用户消息历史
|
||
* @param {string} userId 客户ID
|
||
* @param {number} page 页码
|
||
* @param {number} pageSize 每页数量
|
||
* @returns {Promise} 消息历史数据
|
||
*/
|
||
getUserMessageHistory: (userId, page = 1, pageSize = 20) => executeWithRetry(async () => {
|
||
console.log(`获取用户 ${userId} 的消息历史,页码: ${page}, 每页: ${pageSize}...`);
|
||
|
||
const response = await apiClient.get('/messages', {
|
||
params: {
|
||
from_user: userId,
|
||
page: page,
|
||
page_size: pageSize
|
||
}
|
||
});
|
||
|
||
if (!response.data.success) {
|
||
throw new Error(`获取消息历史失败: ${response.data.message || '未知错误'}`);
|
||
}
|
||
|
||
console.log('消息历史获取成功:', {
|
||
total: response.data.data?.total || 0,
|
||
messagesCount: response.data.data?.messages?.length || 0
|
||
});
|
||
|
||
return response.data.data;
|
||
}, 3, (retryCount) => {
|
||
console.log(`第${retryCount}次重试获取消息历史...`);
|
||
})
|
||
};
|
||
|
||
/**
|
||
* 大语言模型服务
|
||
*/
|
||
export const glmService = {
|
||
/**
|
||
* 发送消息到大语言模型
|
||
* @param {string} message 消息内容
|
||
* @param {string} conversationId 会话ID(可选)
|
||
* @returns {Promise} 模型回复
|
||
*/
|
||
sendMessage: (message, conversationId = null) => executeWithRetry(async () => {
|
||
console.log('发送消息到大语言模型...', { message, conversationId });
|
||
|
||
const payload = {
|
||
message,
|
||
conversation_id: conversationId
|
||
};
|
||
|
||
const response = await apiClient.post('/glm/chat', payload);
|
||
|
||
if (!response.data || !response.data.response) {
|
||
throw new Error('大语言模型返回格式错误');
|
||
}
|
||
|
||
console.log('大语言模型回复成功:', response.data);
|
||
return response.data;
|
||
}, 3, (retryCount) => {
|
||
console.log(`第${retryCount}次重试发送消息到大语言模型...`);
|
||
}),
|
||
|
||
/**
|
||
* 基于客户标签生成智能回复
|
||
* @param {Object} customerInfo 客户信息(包含标签)
|
||
* @returns {Promise} 生成的回复
|
||
*/
|
||
generateReplyFromTags: async (customerInfo) => {
|
||
const prompt = customerService.generatePromptFromTags(customerInfo);
|
||
console.log('生成的提示词:', prompt);
|
||
|
||
const result = await glmService.sendMessage(prompt);
|
||
return result.response;
|
||
},
|
||
|
||
/**
|
||
* 基于客户标签和历史会话生成智能回复
|
||
* @param {Object} customerInfo 客户信息(包含标签)
|
||
* @param {Array} messageHistory 消息历史数据
|
||
* @returns {Promise} 生成的回复
|
||
*/
|
||
generateReplyFromTagsAndHistory: async (customerInfo, messageHistory = []) => {
|
||
const prompt = customerService.generatePromptFromTagsAndHistory(customerInfo, messageHistory);
|
||
console.log('生成的提示词(包含历史):', prompt);
|
||
|
||
const result = await glmService.sendMessage(prompt);
|
||
return result.response;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 工作流服务 - 整合所有步骤
|
||
*/
|
||
export const workflowService = {
|
||
/**
|
||
* 执行工作流但不发送消息(只生成回复)
|
||
* @param {Object} weComHook 企业微信 Hook 实例
|
||
* @param {Function} onProgress 进度回调
|
||
* @returns {Promise} 工作流结果(包含生成的回复)
|
||
*/
|
||
executeWorkflowWithoutSend: async (weComHook, onProgress = null) => {
|
||
const results = {};
|
||
|
||
try {
|
||
// 1. 获取认证信息
|
||
if (onProgress) onProgress('auth', 'loading');
|
||
const [tokenResult, ticketResult] = await Promise.all([
|
||
authService.getAccessToken(),
|
||
authService.getJsapiTicket()
|
||
]);
|
||
results.auth = { token: tokenResult, ticket: ticketResult };
|
||
if (onProgress) onProgress('auth', 'success', results.auth);
|
||
|
||
// 2. 注册企业微信 JSSDK(同步调用)
|
||
if (onProgress) onProgress('register', 'loading');
|
||
let registerResult;
|
||
try {
|
||
const registered = weComHook.registerSdk();
|
||
registerResult = {
|
||
registered,
|
||
message: registered ? 'JSSDK注册调用成功' : 'JSSDK注册调用失败'
|
||
};
|
||
|
||
// 如果注册失败,抛出错误停止工作流
|
||
if (!registered) {
|
||
throw new Error('JSSDK注册失败,无法继续执行需要JSSDK的步骤');
|
||
}
|
||
} catch (error) {
|
||
console.error('JSSDK注册调用时发生错误:', error);
|
||
registerResult = {
|
||
registered: false,
|
||
error: error.message,
|
||
message: `注册调用失败: ${error.message}`
|
||
};
|
||
// 抛出错误停止工作流
|
||
throw new Error(`JSSDK注册失败: ${error.message}`);
|
||
}
|
||
results.register = registerResult;
|
||
if (onProgress) onProgress('register', 'success', registerResult);
|
||
|
||
// 3. 获取客户ID
|
||
if (onProgress) onProgress('customer', 'loading');
|
||
|
||
console.log('开始获取客户ID步骤...');
|
||
|
||
let customerResult;
|
||
try {
|
||
// 首先获取页面场景,检查是否在支持获取客户ID的页面
|
||
let contextInfo = null;
|
||
try {
|
||
contextInfo = await weComHook.getContext();
|
||
console.log('当前页面场景:', contextInfo);
|
||
|
||
// 检查是否获取到有效的页面场景数据
|
||
if (!contextInfo || contextInfo.entry === 'unknown') {
|
||
const errorMessage = `无法获取有效的页面场景信息。可能的原因:\n1. 不在企业微信环境中运行\n2. JSSDK未正确注册\n3. 当前页面不支持获取场景信息\n\n请确保在企业微信中打开此应用,并刷新页面重试。`;
|
||
console.error(errorMessage);
|
||
throw new Error(errorMessage);
|
||
}
|
||
|
||
// 检查 entry 是否有效
|
||
if (!contextInfo.isValidEntry) {
|
||
const errorMessage = `当前页面不支持获取客户ID。getCurExternalContact() 接口只在以下页面可用:\n• 外部联系人详情页面 (contact_profile)\n• 单聊工具栏页面 (single_chat_tools)\n• 单聊客服工具栏页面 (single_kf_tools)\n\n当前页面: ${contextInfo.entry}\n请在企业微信外部联系人聊天页面中打开此应用。`;
|
||
console.error(errorMessage);
|
||
throw new Error(errorMessage);
|
||
}
|
||
|
||
console.log('当前页面支持获取客户ID,entry:', contextInfo.entry);
|
||
// alert(`当前页面支持获取客户ID,entry: ${contextInfo.entry}`);
|
||
|
||
// 在支持获取客户ID的页面,尝试获取真实客户ID
|
||
customerResult = await weComHook.getCurExternalContact()
|
||
.then(customerInfo => {
|
||
console.log('成功获取到客户ID,完整数据:', customerInfo);
|
||
console.log('客户ID字段值:', customerInfo.externalUserId);
|
||
console.log('客户数据所有键名:', Object.keys(customerInfo || {}));
|
||
// alert(`成功获取到客户ID: ${customerInfo.externalUserId}\n完整数据键名: ${Object.keys(customerInfo || {}).join(', ')}`);
|
||
return {
|
||
...customerInfo,
|
||
context: contextInfo,
|
||
mock: false
|
||
};
|
||
})
|
||
.catch(error => {
|
||
console.error('获取客户ID失败:', error);
|
||
throw error; // 重新抛出错误,停止工作流
|
||
});
|
||
|
||
} catch (contextError) {
|
||
console.error('获取页面场景失败或页面不支持获取客户ID:', contextError);
|
||
throw contextError; // 重新抛出错误,停止工作流
|
||
}
|
||
} catch (error) {
|
||
console.error('获取客户ID步骤失败:', error);
|
||
// 不再返回模拟数据,而是抛出错误停止工作流
|
||
throw new Error(`获取客户ID失败: ${error.message}`);
|
||
}
|
||
|
||
results.customer = customerResult;
|
||
if (onProgress) onProgress('customer', 'success', customerResult);
|
||
|
||
// 4. 获取客户标签
|
||
if (onProgress) onProgress('tags', 'loading');
|
||
let tagsResult;
|
||
if (!customerResult.externalUserId || customerResult.mock) {
|
||
console.warn('使用模拟标签数据');
|
||
tagsResult = {
|
||
name: '模拟客户',
|
||
tags: [
|
||
{ TagName: '小红书用户', TagCategory: '渠道' },
|
||
{ TagName: '高意向客户', TagCategory: '意向度' },
|
||
{ TagName: '已购买用户', TagCategory: '客户状态' }
|
||
],
|
||
mock: true,
|
||
message: '使用模拟标签数据'
|
||
};
|
||
} else {
|
||
try {
|
||
tagsResult = await customerService.getCustomerTags(customerResult.externalUserId);
|
||
} catch (error) {
|
||
console.warn('获取客户标签失败,使用模拟数据:', error);
|
||
tagsResult = {
|
||
name: '客户(标签获取失败)',
|
||
tags: [{ TagName: '兜爸心选客户', TagCategory: '默认' }],
|
||
mock: true,
|
||
error: error.message,
|
||
message: '标签获取失败,使用模拟数据'
|
||
};
|
||
}
|
||
}
|
||
results.tags = tagsResult;
|
||
if (onProgress) onProgress('tags', 'success', tagsResult);
|
||
|
||
// 5. 获取会话历史
|
||
if (onProgress) onProgress('history', 'loading');
|
||
let historyResult;
|
||
// alert(`5. 获取会话历史开始 userId: ${customerResult.externalUserId}`);
|
||
if (!customerResult.externalUserId || customerResult.mock) {
|
||
console.warn('使用模拟历史数据');
|
||
historyResult = {
|
||
messages: [
|
||
{
|
||
msgid: 'mock_msg_001',
|
||
from_user: customerResult.externalUserId || 'mock_customer',
|
||
text: { content: '你好,我想了解一下你们的产品' },
|
||
msgtime: new Date(Date.now() - 86400000).toISOString(),
|
||
msgtype: 'text'
|
||
},
|
||
{
|
||
msgid: 'mock_msg_002',
|
||
from_user: '客服',
|
||
text: { content: '您好!欢迎咨询兜爸心选产品,请问您想了解哪方面的信息?' },
|
||
msgtime: new Date(Date.now() - 86300000).toISOString(),
|
||
msgtype: 'text'
|
||
}
|
||
],
|
||
total: 2,
|
||
mock: true,
|
||
message: '使用模拟历史数据'
|
||
};
|
||
} else {
|
||
try {
|
||
historyResult = await messageService.getUserMessageHistory(customerResult.externalUserId, 1, 20);
|
||
} catch (error) {
|
||
console.warn('获取会话历史失败,使用模拟数据:', error);
|
||
historyResult = {
|
||
messages: [],
|
||
total: 0,
|
||
mock: true,
|
||
error: error.message,
|
||
message: '历史获取失败,使用模拟数据'
|
||
};
|
||
}
|
||
}
|
||
results.history = historyResult;
|
||
if (onProgress) onProgress('history', 'success', historyResult);
|
||
|
||
// 6. 生成智能回复
|
||
if (onProgress) onProgress('generate', 'loading');
|
||
let generatedReply;
|
||
try {
|
||
// 合并客户信息和标签信息
|
||
const customerInfo = {
|
||
...customerResult,
|
||
...tagsResult,
|
||
externalUserId: customerResult.externalUserId
|
||
};
|
||
|
||
// 使用新的函数,包含历史数据
|
||
generatedReply = await glmService.generateReplyFromTagsAndHistory(
|
||
customerInfo,
|
||
historyResult.messages || []
|
||
);
|
||
} catch (error) {
|
||
console.warn('生成智能回复失败,使用默认回复:', error);
|
||
generatedReply = `您好!我是兜爸心选的智能客服。抱歉,生成个性化回复时遇到问题:${error.message}。请问您有什么需要帮助的吗?`;
|
||
}
|
||
results.generate = generatedReply;
|
||
if (onProgress) onProgress('generate', 'success', generatedReply);
|
||
|
||
return {
|
||
success: true,
|
||
steps: results,
|
||
generatedMessage: generatedReply,
|
||
timestamp: new Date().toISOString()
|
||
};
|
||
|
||
} catch (error) {
|
||
console.error('工作流执行失败:', error);
|
||
return {
|
||
success: false,
|
||
error: error.message,
|
||
steps: results,
|
||
timestamp: new Date().toISOString()
|
||
};
|
||
}
|
||
}
|
||
};
|
||
|
||
export default {
|
||
authService,
|
||
customerService,
|
||
glmService,
|
||
workflowService
|
||
};
|