365 lines
15 KiB
JavaScript
365 lines
15 KiB
JavaScript
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.5T,CVT燃动版,购买价格约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']
|
||
}
|
||
})
|