Files
chat_app/frontend/src/services/wecomService.js
2025-12-15 17:44:35 +08:00

596 lines
21 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.
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
};