wx/src/store/chat.js

365 lines
15 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 { defineStore } from 'pinia'
import { sendMessage } from '@/api/chat'
import { useSettingsStore } from '@/store/settings'
import { speakText } from '@/utils/speak'
import { useSpeakStore } from '@/store/speak'
const chatModes = {
training: {
name: '寿险代理人AI陪练',
icon: '👨‍💼',
token: 'app-88ae2GN49aUyNO6qGg7tbTfX',
background: `# 客户信息
### **基础背景与性格设定**
**姓名**:刘勇
**年龄**34 岁
**职业**:腾讯员工
**家庭**:妻子为全职太太,女儿 9 岁,双方父母健在
**性格**:冷静、理性、务实,偏好客观数据和逻辑分析,对保险持观望态度,防备心较强
**近期状态**
- 体检显示中度脂肪肝,担心投保问题
- 已为全家配置百万医疗险,妻子有重疾险,但认为保障不足 (保费 50W
- 对理财型保险收益不认可,但对补充重疾险有潜在需求 `,
chatBackground: '通过尚先生介绍,你和他的老同学在他的家里首次面谈。他的家中布置简洁,茶几上摆放着一套茶具。',
},
quote_objection: {
name: '报价中异议',
icon: '💬',
token: 'app-ur2Altw2LHR6niX8Q1S7Cn41',
background: `# 客户信息
### **基础背景与性格设定**
**姓名**: 王大牛
**年龄**: 40岁
**职业**: 私营企业主
**车牌号**: 闽C12345
**车辆信息**拥有一辆行驶4年的大众途观L购买价格约25万主要用于商务出行及家庭使用。
**投保信息**在其他保司购买交强险及商业险三者险、车损险还有1个月到期。有过一次轻微追尾事故已通过保司处理。王先生对成本控制非常敏感但也能意识到保险的重要性。他希望得到保障全面、价格合理的产品和优质的服务。`,
chatBackground: '现在你将扮演坐席专员,与系统扮演的客户针对报价中的各类异议开展对练,着重训练处理"报价中异议"的能力。按照"保全保足"原则给出险种推荐方案。',
},
post_quote_objection: {
name: '报价后异议',
icon: '💰',
token: 'app-Yiccl0JoXs2QF2lkHxO6f822',
background: `# 客户信息
### **基础背景与性格设定**
**姓名**: 张灵女士
**年龄**: 32岁
**职业**: 公司行政主管
**车牌号**: 粤B56789
**车辆信息**拥有一辆行驶4年多的本田思域1.5TCVT燃动版购买价格约15万用于日常通勤及周末短途出行。
**投保信息**在其他保司购买交强险及商业险三者险、车损险、不计免赔险、车上人员责任险还有2个月到期。无事故记录仅有两次违章停车记录。张女士注重性价比和服务质量希望保险的保障全面、价格合理、服务贴心。`,
chatBackground: '现在你将扮演坐席专员,与系统扮演的客户针对报价后的各类异议开展对练,着重训练处理"报价后异议"的能力。按照"保全保足"原则给出险种推荐方案。',
}
}
// 返回示例
/*
data: {"event": "message", "conversation_id": "fac9e3e7-2b2e-4fc2-bf19-0a6d4cfcf529", "message_id": "e8f42230-205c-4f31-b068-86b37a18c2ee", "created_at": 1744881805, "task_id": "ec521a05-6068-427a-ac5d-d349f0d05874", "id": "e8f42230-205c-4f31-b068-86b37a18c2ee", "answer": "\u7684\u65b9\u5411", "from_variable_selector": ["17441814078920", "text"]}
data: {"event": "message", "conversation_id": "fac9e3e7-2b2e-4fc2-bf19-0a6d4cfcf529", "message_id": "e8f42230-205c-4f31-b068-86b37a18c2ee", "created_at": 1744881805, "task_id": "ec521a05-6068-427a-ac5d-d349f0d05874", "id": "e8f42230-205c-4f31-b068-86b37a18c2ee", "answer": "\u3002", "from_variable_selector": ["17441814078920", "text"]}
data: {"event": "message_end", "conversation_id": "fac9e3e7-2b2e-4fc2-bf19-0a6d4cfcf529", "message_id": "e8f42230-205c-4f31-b068-86b37a18c2ee", "created_at": 1744881805, "task_id": "ec521a05-6068-427a-ac5d-d349f0d05874", "id": "e8f42230-205c-4f31-b068-86b37a18c2ee", "metadata": {"usage": {"prompt_tokens": 2483, "prompt_unit_price": "4.13", "prompt_price_unit": "0.000001", "prompt_price": "0.0102548", "completion_tokens": 142, "completion_unit_price": "4.13", "completion_price_unit": "0.000001", "completion_price": "0.0005865", "total_tokens": 2625, "total_price": "0.0108413", "currency": "RMB", "latency": 13.045417829882354}}, "files": []}
*/
const tagList = ['kehu', 'pingfen', 'zongjie', 'dafen']
// 处理标签内容的辅助函数
const processTagContent = (answerCache, tagName, cachedMessage) => {
// 检查是否包含完整的开始标签
const startTag = `<${tagName}>`
const endTag = `</${tagName}>`
// 如果正在处理该标签
if (cachedMessage.respondingType === tagName) {
// 检查是否包含结束标签
if (answerCache.includes(endTag)) {
// 提取结束标签前的内容
const content = answerCache.split(endTag)[0]
cachedMessage[tagName] += content
cachedMessage.respondingType = ''
cachedMessage[`${tagName}Finished`] = true
return answerCache.split(endTag)[1] || ''
} else {
// 继续累积内容
cachedMessage[tagName] += answerCache
return ''
}
}
// 检查是否包含开始标签
if (answerCache.includes(startTag)) {
cachedMessage.respondingType = tagName
const tagContent = answerCache.split(startTag)[1] || ''
// 检查开始标签后是否立即包含结束标签
if (tagContent.includes(endTag)) {
const content = tagContent.split(endTag)[0]
cachedMessage[tagName] = content
cachedMessage.respondingType = ''
return answerCache.split(endTag)[1] || ''
} else {
// 只有开始标签,累积内容
cachedMessage[tagName] = tagContent
return ''
}
}
return answerCache
}
export const useChatStore = defineStore('chat', {
state: () => ({
messages: {},
currentMode: 'training',
conversationId: null,
messageCache: {},
}),
getters: {
chatModes: () => chatModes,
currentToken: (state) => chatModes[state.currentMode].token,
currentConversation: (state) => {
return state.conversationId ? state.messages[state.conversationId] : null
},
currentMessages: (state) => {
// 根据conversationId和currentMode找到对应的messageCache的messageId
const cache = Object.values(state.messageCache).find(message =>
message.mode === state.currentMode && message.conversationId === state.conversationId
)
// 获取当前会话的消息
const currentConversation = state.conversationId ? state.messages[state.conversationId] : null
const conversationMessages = currentConversation ? currentConversation.chatMessages : []
return conversationMessages.filter(message =>
message.mode === state.currentMode &&
(message.conversationId === state.conversationId || !state.conversationId) &&
(!cache || message.messageId !== cache.messageId)
).concat(cache ? [cache] : [])
},
// 获取所有会话列表
conversations: (state) => {
return Object.entries(state.messages).map(([conversationId, conversation]) => ({
conversationId,
mode: conversation.chatModes,
messages: conversation.chatMessages,
status: conversation.conversationStatus,
summary: conversation.summary,
// 使用第一条消息的时间作为会话创建时间
createdAt: conversation.chatMessages[0]?.createdAt || 0
})).sort((a, b) => b.createdAt - a.createdAt) // 按时间倒序排序
}
},
actions: {
setCurrentMode(mode) {
this.currentMode = mode
// 找到当前模式的最后一条会话
const lastConversation = this.conversations
.filter(conv => conv.mode === mode)
.sort((a, b) => b.createdAt - a.createdAt)[0]
// 设置当前会话ID
this.conversationId = lastConversation?.conversationId || null
console.log('%c currentConversation:', 'color: #2196F3; font-weight: bold', {
mode,
lastConversation,
currentConversation: this.currentConversation
})
},
// 切换到指定会话
switchConversation(conversationId) {
if (this.messages[conversationId]) {
this.conversationId = conversationId
// 清除当前的消息缓存
Object.keys(this.messageCache).forEach(messageId => {
if (this.messageCache[messageId].conversationId === conversationId) {
delete this.messageCache[messageId]
}
})
}
},
async sendMessage(question) {
if (!question.trim()) return
// 设置会话状态为正在输入
if (this.conversationId && this.messages[this.conversationId]) {
this.messages[this.conversationId].conversationStatus = 'typing'
}
this.hasStartedResponse = false
const spokenSet = new Set(); // 新增:记录已播报的 messageId
try {
const messageStream = await sendMessage({
inputs: {},
query: question,
response_mode: 'streaming',
conversation_id: this.conversationId,
user: 'sys123'
}, this.currentToken)
let answerCache = ''
for await (const message of messageStream) {
if (!message || !message.event) {
continue
}
switch (message.event) {
case 'workflow_started':
console.log('%c workflow_started:message:', 'color: #2196F3; font-weight: bold', message)
// 创建新消息对象
const currentMessage = {
mode: this.currentMode,
conversationId: message.conversation_id,
messageId: message.message_id,
createdAt: message.created_at,
createdAtTimestamp: '',
question: question,
answerBuffer: '',
showEvaluation: false,
respondingType: '',
kehu: '',
pingfen: '',
zongjie: '',
dafen: '',
kehuFinished: false,
pingfenFinished: false,
zongjieFinished: false,
dafenFinished: false,
}
this.conversationId = message.conversation_id
this.messageCache[message.message_id] = currentMessage
// 初始化会话消息结构
if (!this.messages[currentMessage.conversationId]) {
this.messages[currentMessage.conversationId] = {
chatModes: currentMessage.mode,
chatMessages: [],
conversationStatus: 'typing', // 会话状态 active 活跃finished 已完成, typing 正在输入
//总结
summary: '',
}
}
break
case 'message':
if (message.answer) {
console.log('%c message:answer:', 'color: #2196F3; font-weight: bold', message.answer)
answerCache += message.answer
const cachedMessage = this.messageCache[message.message_id]
if (cachedMessage) {
for (const tag of tagList) {
answerCache = processTagContent(answerCache, tag, cachedMessage)
}
// 只在kehuFinished为true且未播报过时播报
if (
cachedMessage.kehuFinished &&
!spokenSet.has(message.message_id)
) {
const speakStore = useSpeakStore()
if (speakStore.autoTTS) {
speakText(cachedMessage.kehu)
spokenSet.add(message.message_id)
}
}
}
}
break
case 'workflow_finished':
console.log('%c workflow_finished:message:', 'color: #2196F3; font-weight: bold', message)
if (this.messageCache[message.message_id]) {
const cachedMessage = this.messageCache[message.message_id]
if (cachedMessage) {
cachedMessage.respondingType = ''
cachedMessage.answerBuffer = message.data.outputs.answer
this.messages[cachedMessage.conversationId].chatMessages.push(cachedMessage)
// 如果包含dafen标签将会话标记为完成并将打分内容归档到summary
if (cachedMessage.dafen) {
this.messages[cachedMessage.conversationId].conversationStatus = 'finished'
this.messages[cachedMessage.conversationId].summary = cachedMessage.dafen
} else {
// 恢复会话状态为活跃
this.messages[cachedMessage.conversationId].conversationStatus = 'active'
}
console.log('%c workflow_finished:cachedMessage:', 'color: #ff0000; font-weight: bold', cachedMessage)
}
delete this.messageCache[message.message_id]
}
break
}
}
} catch (error) {
console.error('发送消息失败:', error)
throw error
}
},
toggleEvaluation(messageId) {
console.log('%c toggleEvaluation:messageId:', 'color: #2196F3; font-weight: bold', messageId)
const cache = Object.values(this.messageCache).find(message =>
message.mode === this.currentMode && message.messageId === messageId
)
if (cache) {
cache.showEvaluation = !cache.showEvaluation
} else {
// 遍历所有会话查找消息
for (const conversationId in this.messages) {
const conversation = this.messages[conversationId]
const message = conversation.chatMessages.find(msg => msg.messageId === messageId)
if (message) {
message.showEvaluation = !message.showEvaluation
break
}
}
}
},
startNewChat() {
// 如果当前有活跃会话,将其标记为完成
// if (this.conversationId && this.messages[this.conversationId]) {
// this.messages[this.conversationId].conversationStatus = 'finished'
// }
// 只清除当前会话相关的消息缓存
if (this.conversationId) {
// 从messageCache中删除当前会话的消息
Object.keys(this.messageCache).forEach(messageId => {
if (this.messageCache[messageId].conversationId === this.conversationId) {
delete this.messageCache[messageId]
}
})
}
// 清除当前会话ID
this.conversationId = null
},
// 删除指定会话
deleteConversation(conversationId) {
// 如果要删除的是当前会话,先清除相关缓存
if (this.conversationId === conversationId) {
Object.keys(this.messageCache).forEach(messageId => {
if (this.messageCache[messageId].conversationId === conversationId) {
delete this.messageCache[messageId]
}
})
this.conversationId = null
}
// 从messages中删除会话
if (this.messages[conversationId]) {
delete this.messages[conversationId]
}
}
},
persist: {
key: 'chatStore',
storage: sessionStorage,
paths: ['messages', 'currentMode', 'conversationId']
}
})