This commit is contained in:
parent
6ab8f89770
commit
e78a7b95a1
|
|
@ -1,65 +1,213 @@
|
||||||
/**
|
/**
|
||||||
* AI助手相关API接口模块
|
* 宠物助手相关API接口模块
|
||||||
|
* 对接后端宠物助手服务接口
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import BaseRequest from '../utils/request-helper.js'
|
import BaseRequest from '../utils/request-helper.js'
|
||||||
import { LOADING_TEXTS } from '../config/constants.js'
|
import { LOADING_TEXTS } from '../config/constants.js'
|
||||||
|
|
||||||
|
// ==================== 宠物助手聊天接口 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送消息给AI助手
|
* 向宠物助手提问(非流式)
|
||||||
|
* @description 发送消息给宠物助手,获取专业的宠物护理建议
|
||||||
|
* @param {Object} messageData 消息数据对象
|
||||||
|
* @param {string} messageData.message 用户消息内容,必填,1-2000字符
|
||||||
|
* @param {string} [messageData.sessionId] 会话ID,可选,UUID格式
|
||||||
|
* @param {boolean} [messageData.stream=false] 是否流式响应
|
||||||
|
* @param {number} [messageData.temperature=0.7] 温度参数,控制随机性,范围0-2
|
||||||
|
* @param {number} [messageData.maxTokens=1000] 最大生成token数,范围1-4000
|
||||||
|
* @param {string} [messageData.model] 模型名称,可选
|
||||||
|
* @param {Object} [config={}] 自定义请求配置
|
||||||
|
* @returns {Promise<Object>} 返回聊天响应对象
|
||||||
|
* @example
|
||||||
|
* // 基本用法
|
||||||
|
* const response = await askPetAssistant({
|
||||||
|
* message: '我的狗狗不吃饭怎么办?'
|
||||||
|
* })
|
||||||
|
*
|
||||||
|
* // 指定会话和参数
|
||||||
|
* const response = await askPetAssistant({
|
||||||
|
* message: '还有其他建议吗?',
|
||||||
|
* sessionId: 'uuid-session-id',
|
||||||
|
* temperature: 0.8,
|
||||||
|
* maxTokens: 1500
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
export const askPetAssistant = (messageData, config = {}) => {
|
||||||
|
return BaseRequest.post('/pet/user/assistant/ask', messageData, 'AUTHENTICATED_UPDATE', LOADING_TEXTS.AI_THINKING, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向宠物助手流式提问
|
||||||
|
* @description 发送消息给宠物助手,获取流式响应(实时显示回复过程)
|
||||||
|
* @param {Object} messageData 消息数据对象
|
||||||
|
* @param {string} messageData.message 用户消息内容
|
||||||
|
* @param {string} [messageData.sessionId] 会话ID,可选
|
||||||
|
* @param {number} [messageData.temperature=0.7] 温度参数
|
||||||
|
* @param {number} [messageData.maxTokens=1000] 最大token数
|
||||||
|
* @param {Object} [config={}] 自定义请求配置
|
||||||
|
* @returns {Promise<Object>} 返回流式响应对象
|
||||||
|
* @example
|
||||||
|
* const response = await streamAskPetAssistant({
|
||||||
|
* message: '我的猫咪呕吐是什么原因?',
|
||||||
|
* sessionId: 'uuid-session-id'
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
export const streamAskPetAssistant = (messageData, config = {}) => {
|
||||||
|
// 强制设置为流式响应
|
||||||
|
const streamData = { ...messageData, stream: true }
|
||||||
|
return BaseRequest.post('/pet/user/assistant/stream-ask', streamData, 'AUTHENTICATED_UPDATE', LOADING_TEXTS.AI_THINKING, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取宠物助手对话历史
|
||||||
|
* @description 获取用户与宠物助手的对话历史记录(单一会话)
|
||||||
|
* @param {Object} [params={}] 查询参数
|
||||||
|
* @param {number} [params.page=1] 页码,默认第1页
|
||||||
|
* @param {number} [params.pageSize=20] 每页数量,默认20条
|
||||||
|
* @param {Object} [config={}] 自定义请求配置
|
||||||
|
* @returns {Promise<Object>} 返回历史记录列表
|
||||||
|
* @example
|
||||||
|
* // 获取历史记录
|
||||||
|
* const history = await getAssistantHistory({
|
||||||
|
* page: 1,
|
||||||
|
* pageSize: 50
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
export const getAssistantHistory = (params = {}, config = {}) => {
|
||||||
|
return BaseRequest.get('/pet/user/assistant/history', params, 'AUTHENTICATED_QUERY', config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空宠物助手对话历史
|
||||||
|
* @description 清空用户与宠物助手的对话历史记录
|
||||||
|
* @param {Object} [config={}] 自定义请求配置
|
||||||
|
* @returns {Promise<Object>} 返回操作结果
|
||||||
|
* @example
|
||||||
|
* // 清空历史记录
|
||||||
|
* await clearAssistantHistory()
|
||||||
|
*/
|
||||||
|
export const clearAssistantHistory = (config = {}) => {
|
||||||
|
return BaseRequest.delete('/pet/user/assistant/clear-history', {}, 'AUTHENTICATED_DELETE', '正在清除历史记录...', config)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ==================== 兼容性接口(保持向后兼容) ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息给AI助手(兼容旧版本)
|
||||||
|
* @deprecated 请使用 askPetAssistant 替代
|
||||||
* @param {Object} messageData 消息数据
|
* @param {Object} messageData 消息数据
|
||||||
* @param {Object} config 自定义配置
|
* @param {Object} config 自定义配置
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
export const sendMessage = (messageData, config = {}) => {
|
export const sendMessage = (messageData, config = {}) => {
|
||||||
return BaseRequest.post('/ai/chat', messageData, 'AUTHENTICATED_UPDATE', LOADING_TEXTS.AI_THINKING, config)
|
console.warn('sendMessage is deprecated, please use askPetAssistant instead')
|
||||||
|
return askPetAssistant(messageData, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取AI知识库
|
* 获取聊天历史(兼容旧版本)
|
||||||
* @param {Object} params 查询参数
|
* @deprecated 请使用 getAssistantHistory 替代
|
||||||
* @param {Object} config 自定义配置
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
export const getKnowledgeBase = (params = {}, config = {}) => {
|
|
||||||
return BaseRequest.get('/ai/knowledge', params, 'SILENT_REQUEST', config)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取聊天历史
|
|
||||||
* @param {Object} params 查询参数
|
* @param {Object} params 查询参数
|
||||||
* @param {Object} config 自定义配置
|
* @param {Object} config 自定义配置
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
export const getChatHistory = (params = {}, config = {}) => {
|
export const getChatHistory = (params = {}, config = {}) => {
|
||||||
return BaseRequest.get('/ai/chat/history', params, 'AUTHENTICATED_QUERY', config)
|
console.warn('getChatHistory is deprecated, please use getAssistantHistory instead')
|
||||||
|
return getAssistantHistory(params, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清除聊天历史
|
* 清除聊天历史(兼容旧版本)
|
||||||
|
* @deprecated 请使用 clearAssistantHistory 替代
|
||||||
* @param {Object} config 自定义配置
|
* @param {Object} config 自定义配置
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
export const clearChatHistory = (config = {}) => {
|
export const clearChatHistory = (config = {}) => {
|
||||||
return BaseRequest.delete('/ai/chat/history', {}, 'AUTHENTICATED_DELETE', '正在清除历史记录...', config)
|
console.warn('clearChatHistory is deprecated, please use clearAssistantHistory instead')
|
||||||
|
return clearAssistantHistory({}, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 工具函数 ====================
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证消息内容
|
||||||
|
* @description 验证用户输入的消息是否符合要求
|
||||||
|
* @param {string} message 消息内容
|
||||||
|
* @returns {Object} 验证结果 { valid: boolean, error?: string }
|
||||||
|
*/
|
||||||
|
export const validateMessage = (message) => {
|
||||||
|
if (!message || typeof message !== 'string') {
|
||||||
|
return { valid: false, error: '消息内容不能为空' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmedMessage = message.trim()
|
||||||
|
if (trimmedMessage.length === 0) {
|
||||||
|
return { valid: false, error: '消息内容不能为空' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmedMessage.length > 2000) {
|
||||||
|
return { valid: false, error: '消息内容不能超过2000个字符' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取AI建议
|
* 格式化聊天消息
|
||||||
* @param {Object} petData 宠物数据
|
* @description 将API返回的消息格式化为前端显示格式
|
||||||
* @param {Object} config 自定义配置
|
* @param {Object} apiMessage API返回的消息对象
|
||||||
* @returns {Promise}
|
* @returns {Object} 格式化后的消息对象
|
||||||
*/
|
*/
|
||||||
export const getAISuggestion = (petData, config = {}) => {
|
export const formatChatMessage = (apiMessage) => {
|
||||||
return BaseRequest.post('/ai/suggestion', petData, 'AUTHENTICATED_UPDATE', '正在分析宠物状况...', config)
|
return {
|
||||||
|
id: apiMessage.id,
|
||||||
|
type: apiMessage.role === 'user' ? 'user' : 'ai',
|
||||||
|
content: apiMessage.messageContent || apiMessage.message || '',
|
||||||
|
time: formatMessageTime(apiMessage.createdAt || apiMessage.created_at),
|
||||||
|
sessionId: apiMessage.sessionId,
|
||||||
|
isSensitive: apiMessage.isSensitive || false,
|
||||||
|
tokenCount: apiMessage.tokenCount || 0,
|
||||||
|
responseTime: apiMessage.responseTime || 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 语音转文字
|
* 格式化消息时间
|
||||||
* @param {Object} audioData 音频数据
|
* @description 将时间戳或时间字符串格式化为显示格式
|
||||||
* @param {Object} config 自定义配置
|
* @param {string|number|Date} timestamp 时间戳或时间对象
|
||||||
* @returns {Promise}
|
* @returns {string} 格式化后的时间字符串
|
||||||
*/
|
*/
|
||||||
export const speechToText = (audioData, config = {}) => {
|
export const formatMessageTime = (timestamp) => {
|
||||||
return BaseRequest.upload('/ai/speech-to-text', audioData, LOADING_TEXTS.SPEECH_RECOGNITION, config)
|
if (!timestamp) return ''
|
||||||
|
|
||||||
|
const date = new Date(timestamp)
|
||||||
|
const now = new Date()
|
||||||
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||||||
|
const messageDate = new Date(date.getFullYear(), date.getMonth(), date.getDate())
|
||||||
|
|
||||||
|
const timeStr = date.toLocaleTimeString('zh-CN', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (messageDate.getTime() === today.getTime()) {
|
||||||
|
return timeStr
|
||||||
|
} else if (messageDate.getTime() === today.getTime() - 24 * 60 * 60 * 1000) {
|
||||||
|
return `昨天 ${timeStr}`
|
||||||
|
} else {
|
||||||
|
return date.toLocaleDateString('zh-CN', {
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,13 @@
|
||||||
<view class="status-dot"></view>
|
<view class="status-dot"></view>
|
||||||
<text class="status-text">在线</text>
|
<text class="status-text">在线</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="knowledge-icon" @click="openKnowledge">
|
<view class="action-buttons">
|
||||||
<text class="icon-text">📚</text>
|
<view class="action-btn" @click="clearHistory" title="清空历史">
|
||||||
|
<text class="icon-text">🗑️</text>
|
||||||
|
</view>
|
||||||
|
<view class="action-btn" @click="openKnowledge" title="知识库">
|
||||||
|
<text class="icon-text">📚</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
|
@ -114,6 +119,17 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import {
|
||||||
|
askPetAssistant,
|
||||||
|
streamAskPetAssistant,
|
||||||
|
getAssistantHistory,
|
||||||
|
clearAssistantHistory,
|
||||||
|
validateMessage,
|
||||||
|
formatChatMessage,
|
||||||
|
formatMessageTime
|
||||||
|
} from '@/http/api/assistant.js'
|
||||||
|
import { createPetAssistantSSE } from '@/utils/sse.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -121,82 +137,46 @@ export default {
|
||||||
scrollTop: 0,
|
scrollTop: 0,
|
||||||
isThinking: false,
|
isThinking: false,
|
||||||
userAvatar: '/static/user-avatar.png',
|
userAvatar: '/static/user-avatar.png',
|
||||||
messageList: [
|
messageList: []
|
||||||
{
|
|
||||||
type: 'ai',
|
|
||||||
content: '您好!我是您的宠物AI助手🐾\n\n我可以为您解答关于宠物饲养、健康、训练、营养等方面的问题。\n\n**我能帮你做什么:**\n- 🍖 **饮食建议**:营养搭配、食物选择\n- 🏥 **健康咨询**:症状分析、预防措施 \n- 🎾 **训练指导**:行为纠正、技能训练\n- 💡 **日常护理**:清洁、美容、环境\n\n有什么想了解的吗?',
|
|
||||||
time: this.getCurrentTime()
|
|
||||||
}
|
|
||||||
],
|
|
||||||
quickQuestions: [
|
|
||||||
{ id: 1, text: '猫咪不吃饭怎么办?' },
|
|
||||||
{ id: 2, text: '狗狗需要多久洗一次澡?' },
|
|
||||||
{ id: 3, text: '宠物疫苗接种时间' },
|
|
||||||
{ id: 4, text: '如何训练宠物定点上厕所?' },
|
|
||||||
{ id: 5, text: '宠物发烧的症状' },
|
|
||||||
{ id: 6, text: '幼猫喂养注意事项' }
|
|
||||||
],
|
|
||||||
// 知识库数据
|
|
||||||
knowledgeBase: {
|
|
||||||
'喂食': {
|
|
||||||
keywords: ['喂食', '吃饭', '食物', '饮食', '营养'],
|
|
||||||
responses: [
|
|
||||||
'定时定量喂食很重要,成年猫每天2-3次,幼猫3-4次。',
|
|
||||||
'选择优质的宠物食品,避免给宠物吃人类食物。',
|
|
||||||
'确保宠物随时有清洁的饮用水。'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'健康': {
|
|
||||||
keywords: ['健康', '生病', '症状', '发烧', '呕吐', '腹泻'],
|
|
||||||
responses: [
|
|
||||||
'定期体检很重要,建议每年至少一次。',
|
|
||||||
'注意观察宠物的食欲、精神状态和排便情况。',
|
|
||||||
'如果出现异常症状,请及时就医。'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'疫苗': {
|
|
||||||
keywords: ['疫苗', '接种', '免疫', '预防'],
|
|
||||||
responses: [
|
|
||||||
'幼猫幼犬8-12周开始接种疫苗。',
|
|
||||||
'成年宠物每年需要加强免疫。',
|
|
||||||
'常见疫苗包括三联疫苗和狂犬疫苗。'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'训练': {
|
|
||||||
keywords: ['训练', '教育', '行为', '定点', '上厕所'],
|
|
||||||
responses: [
|
|
||||||
'训练需要耐心和一致性。',
|
|
||||||
'使用正向强化,奖励好行为。',
|
|
||||||
'避免体罚,这会让宠物产生恐惧。'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'洗澡': {
|
|
||||||
keywords: ['洗澡', '清洁', '卫生'],
|
|
||||||
responses: [
|
|
||||||
'狗狗一般1-2周洗一次澡,猫咪通常不需要经常洗澡。',
|
|
||||||
'使用宠物专用洗浴用品。',
|
|
||||||
'洗澡后要及时吹干,避免感冒。'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onLoad(options) {
|
||||||
|
// 加载历史记录
|
||||||
|
this.loadChatHistory()
|
||||||
|
},
|
||||||
|
|
||||||
|
onShow() {
|
||||||
|
// 页面显示时滚动到底部
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.scrollToBottom()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
getCurrentTime() {
|
getCurrentTime() {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`
|
return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`
|
||||||
},
|
},
|
||||||
|
|
||||||
askQuestion(question) {
|
|
||||||
this.inputMessage = question
|
|
||||||
this.sendMessage()
|
|
||||||
},
|
|
||||||
|
|
||||||
sendMessage() {
|
|
||||||
|
async sendMessage() {
|
||||||
if (!this.inputMessage.trim() || this.isThinking) return
|
if (!this.inputMessage.trim() || this.isThinking) return
|
||||||
|
|
||||||
|
// 验证消息内容
|
||||||
|
const validation = validateMessage(this.inputMessage)
|
||||||
|
if (!validation.valid) {
|
||||||
|
uni.showToast({
|
||||||
|
title: validation.error,
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 保存用户输入内容
|
// 保存用户输入内容
|
||||||
const userInput = this.inputMessage
|
const userInput = this.inputMessage.trim()
|
||||||
|
|
||||||
// 添加用户消息
|
// 添加用户消息
|
||||||
const userMessage = {
|
const userMessage = {
|
||||||
|
|
@ -224,46 +204,203 @@ export default {
|
||||||
}, 500)
|
}, 500)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 模拟AI回复
|
try {
|
||||||
setTimeout(() => {
|
// 使用SSE流式响应
|
||||||
const aiMessage = {
|
const response = await this.handleStreamResponse({
|
||||||
type: 'ai',
|
message: userInput,
|
||||||
content: this.getAIResponse(userInput),
|
temperature: 0.7,
|
||||||
time: this.getCurrentTime()
|
maxTokens: 1000
|
||||||
}
|
|
||||||
this.messageList.push(aiMessage)
|
|
||||||
this.isThinking = false
|
|
||||||
|
|
||||||
// AI回复完成后强制滚动到底部
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.forceScrollToBottom()
|
|
||||||
// 延迟再次滚动,确保消息完全显示
|
|
||||||
setTimeout(() => {
|
|
||||||
this.forceScrollToBottom()
|
|
||||||
}, 100)
|
|
||||||
})
|
})
|
||||||
}, 1500 + Math.random() * 1000) // 1.5-2.5秒随机延迟
|
|
||||||
|
// 检查是否包含敏感词
|
||||||
|
if (response && response.data && response.data.isSensitive) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '消息包含敏感内容',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 结束思考状态
|
||||||
|
this.isThinking = false
|
||||||
|
} catch (error) {
|
||||||
|
console.error('SSE流式请求失败,尝试普通请求:', error)
|
||||||
|
|
||||||
|
// 降级处理:使用普通API
|
||||||
|
try {
|
||||||
|
const fallbackResponse = await askPetAssistant({
|
||||||
|
message: userInput,
|
||||||
|
temperature: 0.7,
|
||||||
|
maxTokens: 1000
|
||||||
|
})
|
||||||
|
|
||||||
|
if (fallbackResponse && fallbackResponse.data) {
|
||||||
|
const aiMessage = {
|
||||||
|
type: 'ai',
|
||||||
|
content: fallbackResponse.data.message || fallbackResponse.data.content || '抱歉,我现在无法回答您的问题。',
|
||||||
|
time: this.getCurrentTime(),
|
||||||
|
isSensitive: fallbackResponse.data.isSensitive || false,
|
||||||
|
tokenCount: fallbackResponse.data.tokenCount || 0,
|
||||||
|
responseTime: fallbackResponse.data.responseTime || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 检查敏感词
|
||||||
|
if (fallbackResponse.data.isSensitive) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '消息包含敏感内容',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.messageList.push(aiMessage)
|
||||||
|
this.forceScrollToBottom()
|
||||||
|
} else {
|
||||||
|
this.handleApiError('API响应格式异常')
|
||||||
|
}
|
||||||
|
} catch (fallbackError) {
|
||||||
|
console.error('普通请求也失败:', fallbackError)
|
||||||
|
this.handleApiError(fallbackError.message || '网络请求失败')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// 确保思考状态结束(防止异常情况)
|
||||||
|
this.isThinking = false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getAIResponse(question) {
|
/**
|
||||||
// 智能匹配知识库
|
* 处理API错误
|
||||||
for (let category in this.knowledgeBase) {
|
* @param {string} errorMessage 错误信息
|
||||||
const { keywords, responses } = this.knowledgeBase[category]
|
*/
|
||||||
if (keywords.some(keyword => question.includes(keyword))) {
|
handleApiError(errorMessage) {
|
||||||
const randomResponse = responses[Math.floor(Math.random() * responses.length)]
|
const errorMessage_display = {
|
||||||
return `${randomResponse}\n\n💡 如果您需要更详细的建议,建议咨询专业的宠物医生。`
|
type: 'ai',
|
||||||
}
|
content: `抱歉,我遇到了一些问题:${errorMessage}\n\n请稍后再试,或者检查网络连接。`,
|
||||||
|
time: this.getCurrentTime(),
|
||||||
|
isError: true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 默认回复
|
// 直接添加错误消息
|
||||||
const defaultResponses = [
|
this.messageList.push(errorMessage_display)
|
||||||
'这是一个很好的问题!建议您咨询专业的宠物医生获得更准确的建议。',
|
this.forceScrollToBottom()
|
||||||
'每只宠物的情况都不同,建议根据具体情况来处理。您可以查看我们的知识库了解更多信息。',
|
},
|
||||||
'感谢您的提问!这个问题比较复杂,建议您带宠物去专业的宠物医院检查。',
|
|
||||||
'我理解您的担心。建议您记录宠物的具体症状,然后咨询专业医生。'
|
|
||||||
]
|
|
||||||
|
|
||||||
return defaultResponses[Math.floor(Math.random() * defaultResponses.length)]
|
/**
|
||||||
|
* 加载聊天历史记录
|
||||||
|
*/
|
||||||
|
async loadChatHistory() {
|
||||||
|
try {
|
||||||
|
const response = await getAssistantHistory({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 50
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response && response.data && response.data.list && response.data.list.length > 0) {
|
||||||
|
// 格式化并设置历史消息
|
||||||
|
const historyMessages = response.data.list.map(msg => formatChatMessage(msg))
|
||||||
|
this.messageList = historyMessages
|
||||||
|
|
||||||
|
// 滚动到底部
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.scrollToBottom()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 没有历史记录,保持空列表
|
||||||
|
this.messageList = []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载历史记录失败:', error)
|
||||||
|
// 静默处理,保持空列表
|
||||||
|
this.messageList = []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空聊天历史
|
||||||
|
*/
|
||||||
|
async clearHistory() {
|
||||||
|
try {
|
||||||
|
const result = await uni.showModal({
|
||||||
|
title: '确认清空',
|
||||||
|
content: '确定要清空聊天记录吗?此操作不可恢复。',
|
||||||
|
confirmText: '清空',
|
||||||
|
cancelText: '取消'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result.confirm) return
|
||||||
|
|
||||||
|
uni.showLoading({
|
||||||
|
title: '正在清空...'
|
||||||
|
})
|
||||||
|
|
||||||
|
await clearAssistantHistory()
|
||||||
|
|
||||||
|
// 清空消息列表
|
||||||
|
this.messageList = []
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: '历史记录已清空',
|
||||||
|
icon: 'success',
|
||||||
|
duration: 1500
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清空历史记录失败:', error)
|
||||||
|
uni.showToast({
|
||||||
|
title: '清空失败',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
uni.hideLoading()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理SSE流式响应
|
||||||
|
* @param {Object} messageData 消息数据
|
||||||
|
* @returns {Promise} 返回流式响应处理结果
|
||||||
|
*/
|
||||||
|
async handleStreamResponse(messageData) {
|
||||||
|
// 创建AI消息对象
|
||||||
|
const aiMessage = {
|
||||||
|
type: 'ai',
|
||||||
|
content: '',
|
||||||
|
time: this.getCurrentTime(),
|
||||||
|
isSensitive: false,
|
||||||
|
tokenCount: 0,
|
||||||
|
responseTime: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到消息列表
|
||||||
|
this.messageList.push(aiMessage)
|
||||||
|
|
||||||
|
// 使用SSE工具类处理流式响应
|
||||||
|
return createPetAssistantSSE(messageData, {
|
||||||
|
onChunk: (chunk) => {
|
||||||
|
// 实时更新消息内容
|
||||||
|
aiMessage.content = chunk.totalContent
|
||||||
|
this.forceScrollToBottom()
|
||||||
|
},
|
||||||
|
onComplete: (result) => {
|
||||||
|
// 更新消息的元数据
|
||||||
|
if (result.data.isSensitive !== undefined) {
|
||||||
|
aiMessage.isSensitive = result.data.isSensitive
|
||||||
|
}
|
||||||
|
if (result.data.tokenCount !== undefined) {
|
||||||
|
aiMessage.tokenCount = result.data.tokenCount
|
||||||
|
}
|
||||||
|
if (result.data.responseTime !== undefined) {
|
||||||
|
aiMessage.responseTime = result.data.responseTime
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('SSE流式响应失败:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
scrollToBottom() {
|
scrollToBottom() {
|
||||||
|
|
@ -302,19 +439,11 @@ export default {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
startVoiceInput() {
|
|
||||||
uni.showToast({
|
|
||||||
title: '语音功能开发中',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
openKnowledge() {
|
openKnowledge() {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: '/pages/assistant/knowledge'
|
url: '/pages/assistant/knowledge'
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -398,18 +527,30 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.knowledge-icon {
|
.action-buttons {
|
||||||
width: 60rpx;
|
|
||||||
height: 60rpx;
|
|
||||||
border-radius: 30rpx;
|
|
||||||
background: rgba(255, 255, 255, 0.8);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 16rpx;
|
||||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
|
|
||||||
|
|
||||||
.icon-text {
|
.action-btn {
|
||||||
font-size: 28rpx;
|
width: 56rpx;
|
||||||
|
height: 56rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
/**
|
||||||
|
* SSE工具类测试文件
|
||||||
|
* 用于验证SSE功能是否正常工作
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createSSERequest, createPetAssistantSSE, SSE_STATUS } from './sse.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试基础SSE功能
|
||||||
|
*/
|
||||||
|
export function testBasicSSE() {
|
||||||
|
console.log('开始测试基础SSE功能...')
|
||||||
|
|
||||||
|
const testData = {
|
||||||
|
message: '测试消息',
|
||||||
|
temperature: 0.7,
|
||||||
|
maxTokens: 100
|
||||||
|
}
|
||||||
|
|
||||||
|
let receivedChunks = 0
|
||||||
|
let totalContent = ''
|
||||||
|
|
||||||
|
return createSSERequest({
|
||||||
|
url: `${uni.$u.http.config.baseURL}/pet/user/assistant/stream-ask`,
|
||||||
|
data: testData,
|
||||||
|
onChunk: (chunk) => {
|
||||||
|
receivedChunks++
|
||||||
|
totalContent = chunk.totalContent
|
||||||
|
console.log(`接收到第${receivedChunks}个数据块:`, chunk.content)
|
||||||
|
},
|
||||||
|
onComplete: (result) => {
|
||||||
|
console.log('SSE响应完成:', {
|
||||||
|
totalChunks: receivedChunks,
|
||||||
|
totalContent: totalContent,
|
||||||
|
metadata: result.data
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('SSE测试失败:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试宠物助手专用SSE
|
||||||
|
*/
|
||||||
|
export function testPetAssistantSSE() {
|
||||||
|
console.log('开始测试宠物助手SSE功能...')
|
||||||
|
|
||||||
|
const testData = {
|
||||||
|
message: '你好,请介绍一下自己',
|
||||||
|
temperature: 0.7,
|
||||||
|
maxTokens: 200
|
||||||
|
}
|
||||||
|
|
||||||
|
let chunkCount = 0
|
||||||
|
|
||||||
|
return createPetAssistantSSE(testData, {
|
||||||
|
onChunk: (chunk) => {
|
||||||
|
chunkCount++
|
||||||
|
console.log(`宠物助手SSE - 第${chunkCount}块:`, chunk.content)
|
||||||
|
},
|
||||||
|
onComplete: (result) => {
|
||||||
|
console.log('宠物助手SSE完成:', {
|
||||||
|
chunks: chunkCount,
|
||||||
|
message: result.data.message,
|
||||||
|
isSensitive: result.data.isSensitive,
|
||||||
|
tokenCount: result.data.tokenCount
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('宠物助手SSE失败:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行所有测试
|
||||||
|
*/
|
||||||
|
export async function runAllTests() {
|
||||||
|
console.log('=== 开始SSE工具类测试 ===')
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 测试基础SSE
|
||||||
|
console.log('\n1. 测试基础SSE功能')
|
||||||
|
await testBasicSSE()
|
||||||
|
|
||||||
|
// 等待一秒
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
|
||||||
|
// 测试宠物助手SSE
|
||||||
|
console.log('\n2. 测试宠物助手SSE功能')
|
||||||
|
await testPetAssistantSSE()
|
||||||
|
|
||||||
|
console.log('\n=== SSE工具类测试完成 ===')
|
||||||
|
return true
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('SSE测试过程中发生错误:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简单的功能验证
|
||||||
|
*/
|
||||||
|
export function validateSSEFunctions() {
|
||||||
|
console.log('验证SSE工具类函数是否存在...')
|
||||||
|
|
||||||
|
const checks = [
|
||||||
|
{ name: 'createSSERequest', func: createSSERequest },
|
||||||
|
{ name: 'createPetAssistantSSE', func: createPetAssistantSSE },
|
||||||
|
{ name: 'SSE_STATUS', obj: SSE_STATUS }
|
||||||
|
]
|
||||||
|
|
||||||
|
let allValid = true
|
||||||
|
|
||||||
|
checks.forEach(check => {
|
||||||
|
if (typeof check.func === 'function' || typeof check.obj === 'object') {
|
||||||
|
console.log(`✅ ${check.name} - 正常`)
|
||||||
|
} else {
|
||||||
|
console.error(`❌ ${check.name} - 缺失或类型错误`)
|
||||||
|
allValid = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (allValid) {
|
||||||
|
console.log('✅ 所有SSE工具类函数验证通过')
|
||||||
|
} else {
|
||||||
|
console.error('❌ SSE工具类函数验证失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
return allValid
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
testBasicSSE,
|
||||||
|
testPetAssistantSSE,
|
||||||
|
runAllTests,
|
||||||
|
validateSSEFunctions
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,216 @@
|
||||||
|
# SSE工具类使用指南
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
SSE (Server-Sent Events) 工具类提供了通用的流式响应处理功能,支持实时数据流处理、回调机制和错误处理。
|
||||||
|
|
||||||
|
## 主要功能
|
||||||
|
|
||||||
|
### 1. 基础SSE请求 - `createSSERequest()`
|
||||||
|
|
||||||
|
最灵活的底层API,支持完全自定义配置。
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { createSSERequest } from '@/utils/sse.js'
|
||||||
|
|
||||||
|
const response = await createSSERequest({
|
||||||
|
url: 'https://api.example.com/stream',
|
||||||
|
method: 'POST',
|
||||||
|
data: { message: '你好' },
|
||||||
|
headers: { 'Custom-Header': 'value' },
|
||||||
|
onChunk: (chunk) => {
|
||||||
|
console.log('接收到数据:', chunk.content)
|
||||||
|
console.log('累积内容:', chunk.totalContent)
|
||||||
|
},
|
||||||
|
onComplete: (result) => {
|
||||||
|
console.log('流式响应完成:', result.data)
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('发生错误:', error)
|
||||||
|
},
|
||||||
|
doneMarker: '[DONE]' // 自定义结束标记
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 默认配置SSE请求 - `createDefaultSSERequest()`
|
||||||
|
|
||||||
|
带有默认配置的便捷API。
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { createDefaultSSERequest } from '@/utils/sse.js'
|
||||||
|
|
||||||
|
const response = await createDefaultSSERequest(
|
||||||
|
'/api/chat/stream', // API端点
|
||||||
|
{ message: '你好' }, // 请求数据
|
||||||
|
{
|
||||||
|
onChunk: (chunk) => {
|
||||||
|
// 处理数据块
|
||||||
|
updateUI(chunk.content)
|
||||||
|
},
|
||||||
|
onComplete: (result) => {
|
||||||
|
// 处理完成
|
||||||
|
console.log('完成:', result.data.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 宠物助手专用SSE - `createPetAssistantSSE()`
|
||||||
|
|
||||||
|
专门为宠物助手功能设计的API。
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { createPetAssistantSSE } from '@/utils/sse.js'
|
||||||
|
|
||||||
|
const response = await createPetAssistantSSE(
|
||||||
|
{
|
||||||
|
message: '我的宠物生病了怎么办?',
|
||||||
|
temperature: 0.7,
|
||||||
|
maxTokens: 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onChunk: (chunk) => {
|
||||||
|
// 实时更新AI回复
|
||||||
|
aiMessage.content = chunk.totalContent
|
||||||
|
scrollToBottom()
|
||||||
|
},
|
||||||
|
onComplete: (result) => {
|
||||||
|
// 更新元数据
|
||||||
|
aiMessage.isSensitive = result.data.isSensitive
|
||||||
|
aiMessage.tokenCount = result.data.tokenCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 可取消的SSE请求 - `createCancellableSSE()`
|
||||||
|
|
||||||
|
支持取消操作的SSE请求。
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { createCancellableSSE, SSE_STATUS } from '@/utils/sse.js'
|
||||||
|
|
||||||
|
const { promise, cancel, getStatus } = createCancellableSSE({
|
||||||
|
url: '/api/stream',
|
||||||
|
data: { message: '长时间处理的请求' },
|
||||||
|
onChunk: (chunk) => {
|
||||||
|
console.log('状态:', getStatus()) // 'streaming'
|
||||||
|
updateContent(chunk.content)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 5秒后取消请求
|
||||||
|
setTimeout(() => {
|
||||||
|
if (getStatus() === SSE_STATUS.STREAMING) {
|
||||||
|
cancel()
|
||||||
|
console.log('请求已取消')
|
||||||
|
}
|
||||||
|
}, 5000)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await promise
|
||||||
|
console.log('请求完成:', result)
|
||||||
|
} catch (error) {
|
||||||
|
console.log('请求被取消或失败:', error)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 回调函数参数
|
||||||
|
|
||||||
|
### onChunk(chunk)
|
||||||
|
- `chunk.content` - 当前数据块内容
|
||||||
|
- `chunk.totalContent` - 累积的完整内容
|
||||||
|
- `chunk.chunk` - 原始解析的数据对象
|
||||||
|
|
||||||
|
### onComplete(result)
|
||||||
|
- `result.data.message` - 完整的响应消息
|
||||||
|
- `result.data.isSensitive` - 是否包含敏感内容
|
||||||
|
- `result.data.tokenCount` - 使用的token数量
|
||||||
|
- `result.data.responseTime` - 响应时间
|
||||||
|
|
||||||
|
### onError(error)
|
||||||
|
- 网络错误、解析错误或其他异常
|
||||||
|
|
||||||
|
## 状态枚举
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { SSE_STATUS } from '@/utils/sse.js'
|
||||||
|
|
||||||
|
SSE_STATUS.PENDING // 等待中
|
||||||
|
SSE_STATUS.STREAMING // 流式传输中
|
||||||
|
SSE_STATUS.COMPLETED // 已完成
|
||||||
|
SSE_STATUS.ERROR // 发生错误
|
||||||
|
SSE_STATUS.CANCELLED // 已取消
|
||||||
|
```
|
||||||
|
|
||||||
|
## 在Vue组件中使用
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 在组件中导入
|
||||||
|
import { createPetAssistantSSE } from '@/utils/sse.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
async sendMessage() {
|
||||||
|
// 创建消息对象
|
||||||
|
const aiMessage = {
|
||||||
|
type: 'ai',
|
||||||
|
content: '',
|
||||||
|
time: this.getCurrentTime()
|
||||||
|
}
|
||||||
|
this.messageList.push(aiMessage)
|
||||||
|
|
||||||
|
// 发起SSE请求
|
||||||
|
try {
|
||||||
|
const result = await createPetAssistantSSE(
|
||||||
|
{ message: this.inputMessage },
|
||||||
|
{
|
||||||
|
onChunk: (chunk) => {
|
||||||
|
// 实时更新内容
|
||||||
|
aiMessage.content = chunk.totalContent
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.scrollToBottom()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onComplete: (result) => {
|
||||||
|
// 更新元数据
|
||||||
|
Object.assign(aiMessage, result.data)
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('SSE失败:', error)
|
||||||
|
// 可以在这里实现降级处理
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
// 处理异常
|
||||||
|
this.handleError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **自动认证**: 工具类会自动添加Authorization头部
|
||||||
|
2. **错误处理**: 建议同时实现onError回调和try-catch
|
||||||
|
3. **内存管理**: 长时间的流式响应注意内存使用
|
||||||
|
4. **网络状态**: 考虑网络中断的情况
|
||||||
|
5. **降级处理**: SSE失败时可以降级到普通API请求
|
||||||
|
|
||||||
|
## 扩展使用
|
||||||
|
|
||||||
|
可以基于基础API创建更多专用函数:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 创建客服聊天SSE
|
||||||
|
export function createCustomerServiceSSE(data, callbacks) {
|
||||||
|
return createDefaultSSERequest('/api/customer/stream', data, callbacks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建代码生成SSE
|
||||||
|
export function createCodeGenerationSSE(data, callbacks) {
|
||||||
|
return createDefaultSSERequest('/api/code/generate', data, callbacks)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,258 @@
|
||||||
|
/**
|
||||||
|
* SSE (Server-Sent Events) 流式响应工具类
|
||||||
|
* 提供通用的SSE数据流处理功能
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建SSE流式请求
|
||||||
|
* @param {Object} options 配置选项
|
||||||
|
* @param {string} options.url 请求URL
|
||||||
|
* @param {string} options.method 请求方法,默认POST
|
||||||
|
* @param {Object} options.data 请求数据
|
||||||
|
* @param {Object} options.headers 请求头,会自动添加SSE相关头部
|
||||||
|
* @param {Function} options.onChunk 接收到数据块时的回调 (chunk) => {}
|
||||||
|
* @param {Function} options.onComplete 流式响应完成时的回调 (result) => {}
|
||||||
|
* @param {Function} options.onError 发生错误时的回调 (error) => {}
|
||||||
|
* @param {string} options.doneMarker 结束标记,默认为 '[DONE]'
|
||||||
|
* @returns {Promise} 返回Promise,resolve时返回完整结果
|
||||||
|
*/
|
||||||
|
export function createSSERequest(options) {
|
||||||
|
const {
|
||||||
|
url,
|
||||||
|
method = 'POST',
|
||||||
|
data = {},
|
||||||
|
headers = {},
|
||||||
|
onChunk,
|
||||||
|
onComplete,
|
||||||
|
onError,
|
||||||
|
doneMarker = '[DONE]'
|
||||||
|
} = options
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// 构建完整的请求头
|
||||||
|
const requestHeaders = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Authorization': `Bearer ${uni.getStorageSync('token') || ''}`,
|
||||||
|
...headers
|
||||||
|
}
|
||||||
|
|
||||||
|
// 累积的响应数据
|
||||||
|
let accumulatedData = {
|
||||||
|
content: '',
|
||||||
|
metadata: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发起uni.request请求
|
||||||
|
const requestTask = uni.request({
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
data,
|
||||||
|
header: requestHeaders,
|
||||||
|
responseType: 'text',
|
||||||
|
success: (res) => {
|
||||||
|
try {
|
||||||
|
// 处理SSE数据流
|
||||||
|
const lines = res.data.split('\n')
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('data: ')) {
|
||||||
|
const data = line.substring(6).trim()
|
||||||
|
|
||||||
|
// 检查是否为结束标记
|
||||||
|
if (data === doneMarker) {
|
||||||
|
// 流式响应结束
|
||||||
|
const result = {
|
||||||
|
data: {
|
||||||
|
message: accumulatedData.content,
|
||||||
|
...accumulatedData.metadata
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用完成回调
|
||||||
|
if (onComplete) {
|
||||||
|
onComplete(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(data)
|
||||||
|
|
||||||
|
// 处理内容数据
|
||||||
|
if (parsed.content) {
|
||||||
|
accumulatedData.content += parsed.content
|
||||||
|
|
||||||
|
// 调用数据块回调
|
||||||
|
if (onChunk) {
|
||||||
|
onChunk({
|
||||||
|
content: parsed.content,
|
||||||
|
totalContent: accumulatedData.content,
|
||||||
|
chunk: parsed
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新元数据
|
||||||
|
if (parsed.isSensitive !== undefined) {
|
||||||
|
accumulatedData.metadata.isSensitive = parsed.isSensitive
|
||||||
|
}
|
||||||
|
if (parsed.tokenCount !== undefined) {
|
||||||
|
accumulatedData.metadata.tokenCount = parsed.tokenCount
|
||||||
|
}
|
||||||
|
if (parsed.responseTime !== undefined) {
|
||||||
|
accumulatedData.metadata.responseTime = parsed.responseTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存其他元数据
|
||||||
|
Object.keys(parsed).forEach(key => {
|
||||||
|
if (!['content', 'isSensitive', 'tokenCount', 'responseTime'].includes(key)) {
|
||||||
|
accumulatedData.metadata[key] = parsed[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (parseError) {
|
||||||
|
console.warn('解析SSE数据失败:', parseError, data)
|
||||||
|
// 继续处理其他数据,不中断流程
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有收到结束标记,也认为完成
|
||||||
|
const result = {
|
||||||
|
data: {
|
||||||
|
message: accumulatedData.content,
|
||||||
|
...accumulatedData.metadata
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onComplete) {
|
||||||
|
onComplete(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(result)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('处理SSE响应失败:', error)
|
||||||
|
|
||||||
|
if (onError) {
|
||||||
|
onError(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
console.error('SSE请求失败:', error)
|
||||||
|
|
||||||
|
if (onError) {
|
||||||
|
onError(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 返回请求任务,允许外部取消请求
|
||||||
|
return requestTask
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建带有默认配置的SSE请求
|
||||||
|
* @param {string} endpoint API端点路径
|
||||||
|
* @param {Object} data 请求数据
|
||||||
|
* @param {Object} callbacks 回调函数 {onChunk, onComplete, onError}
|
||||||
|
* @returns {Promise} SSE请求Promise
|
||||||
|
*/
|
||||||
|
export function createDefaultSSERequest(endpoint, data, callbacks = {}) {
|
||||||
|
const baseURL = uni.$u?.http?.config?.baseURL || ''
|
||||||
|
|
||||||
|
return createSSERequest({
|
||||||
|
url: `${baseURL}${endpoint}`,
|
||||||
|
method: 'POST',
|
||||||
|
data,
|
||||||
|
...callbacks
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 专用于宠物助手的SSE请求
|
||||||
|
* @param {Object} data 请求数据
|
||||||
|
* @param {Object} callbacks 回调函数
|
||||||
|
* @returns {Promise} SSE请求Promise
|
||||||
|
*/
|
||||||
|
export function createPetAssistantSSE(data, callbacks = {}) {
|
||||||
|
return createDefaultSSERequest('/pet/user/assistant/stream-ask', data, callbacks)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSE请求状态枚举
|
||||||
|
*/
|
||||||
|
export const SSE_STATUS = {
|
||||||
|
PENDING: 'pending',
|
||||||
|
STREAMING: 'streaming',
|
||||||
|
COMPLETED: 'completed',
|
||||||
|
ERROR: 'error',
|
||||||
|
CANCELLED: 'cancelled'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建可取消的SSE请求
|
||||||
|
* @param {Object} options 配置选项
|
||||||
|
* @returns {Object} 返回包含promise和cancel方法的对象
|
||||||
|
*/
|
||||||
|
export function createCancellableSSE(options) {
|
||||||
|
let requestTask = null
|
||||||
|
let status = SSE_STATUS.PENDING
|
||||||
|
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
|
const enhancedOptions = {
|
||||||
|
...options,
|
||||||
|
onChunk: (chunk) => {
|
||||||
|
status = SSE_STATUS.STREAMING
|
||||||
|
if (options.onChunk) {
|
||||||
|
options.onChunk(chunk)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onComplete: (result) => {
|
||||||
|
status = SSE_STATUS.COMPLETED
|
||||||
|
if (options.onComplete) {
|
||||||
|
options.onComplete(result)
|
||||||
|
}
|
||||||
|
resolve(result)
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
status = SSE_STATUS.ERROR
|
||||||
|
if (options.onError) {
|
||||||
|
options.onError(error)
|
||||||
|
}
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestTask = createSSERequest(enhancedOptions)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
promise,
|
||||||
|
cancel: () => {
|
||||||
|
if (requestTask && status === SSE_STATUS.STREAMING) {
|
||||||
|
requestTask.abort?.()
|
||||||
|
status = SSE_STATUS.CANCELLED
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getStatus: () => status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
createSSERequest,
|
||||||
|
createDefaultSSERequest,
|
||||||
|
createPetAssistantSSE,
|
||||||
|
createCancellableSSE,
|
||||||
|
SSE_STATUS
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue