添加用户消息记录接口;

This commit is contained in:
karlcarlo
2025-12-11 03:53:47 +08:00
parent 6029da058a
commit 420fed4bcf
49 changed files with 7574 additions and 426 deletions

View File

@@ -0,0 +1,590 @@
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);
// 在支持获取客户ID的页面,尝试获取真实客户ID
customerResult = await weComHook.getCurExternalContact()
.then(customerInfo => {
console.log('成功获取到客户ID:', customerInfo);
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;
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
};