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 };