This commit is contained in:
parent
e78a7b95a1
commit
cb710b47df
46
App.vue
46
App.vue
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { wxLogin } from '@/http/api/auth.js'
|
import { wxLogin } from '@/http/api/auth.js'
|
||||||
|
import { WebSocketManager } from '@/utils/websocket.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
onLaunch: function() {
|
onLaunch: function() {
|
||||||
|
|
@ -42,6 +43,8 @@
|
||||||
// 检查本地是否有有效token
|
// 检查本地是否有有效token
|
||||||
const token = uni.getStorageSync('token')
|
const token = uni.getStorageSync('token')
|
||||||
if (token) {
|
if (token) {
|
||||||
|
// 已有token,初始化WebSocket连接
|
||||||
|
await this.initWebSocketConnection(token)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,7 +77,7 @@
|
||||||
|
|
||||||
if (result && result.token) {
|
if (result && result.token) {
|
||||||
// 用户已授权过手机号,保存登录信息
|
// 用户已授权过手机号,保存登录信息
|
||||||
this.saveLoginData(result)
|
await this.saveLoginData(result)
|
||||||
} else {
|
} else {
|
||||||
// 用户未授权手机号,保存微信登录code供后续使用
|
// 用户未授权手机号,保存微信登录code供后续使用
|
||||||
this.saveWxCode(loginRes.code)
|
this.saveWxCode(loginRes.code)
|
||||||
|
|
@ -87,7 +90,7 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
// 保存登录数据
|
// 保存登录数据
|
||||||
saveLoginData(result) {
|
async saveLoginData(result) {
|
||||||
uni.setStorageSync('token', result.token)
|
uni.setStorageSync('token', result.token)
|
||||||
if (result.refreshToken) {
|
if (result.refreshToken) {
|
||||||
uni.setStorageSync('refreshToken', result.refreshToken)
|
uni.setStorageSync('refreshToken', result.refreshToken)
|
||||||
|
|
@ -102,6 +105,9 @@
|
||||||
? 'profile_completed'
|
? 'profile_completed'
|
||||||
: 'phone_authed'
|
: 'phone_authed'
|
||||||
uni.setStorageSync('loginStep', loginStep)
|
uni.setStorageSync('loginStep', loginStep)
|
||||||
|
|
||||||
|
// 初始化WebSocket连接
|
||||||
|
await this.initWebSocketConnection(result.token)
|
||||||
},
|
},
|
||||||
|
|
||||||
// 保存微信code
|
// 保存微信code
|
||||||
|
|
@ -109,6 +115,42 @@
|
||||||
uni.setStorageSync('wxLoginCode', code)
|
uni.setStorageSync('wxLoginCode', code)
|
||||||
uni.setStorageSync('loginDate', new Date().toISOString())
|
uni.setStorageSync('loginDate', new Date().toISOString())
|
||||||
uni.setStorageSync('loginStep', 'wx_logged')
|
uni.setStorageSync('loginStep', 'wx_logged')
|
||||||
|
},
|
||||||
|
|
||||||
|
// 初始化WebSocket连接
|
||||||
|
async initWebSocketConnection(token) {
|
||||||
|
try {
|
||||||
|
console.log('🔌 开始初始化WebSocket连接, token:', token ? '已提供' : '未提供')
|
||||||
|
const wsManager = WebSocketManager.getInstance()
|
||||||
|
|
||||||
|
// 获取连接信息用于调试
|
||||||
|
const connectionInfo = wsManager.getConnectionInfo()
|
||||||
|
console.log('📊 WebSocket连接信息:', connectionInfo)
|
||||||
|
|
||||||
|
// 建立WebSocket连接
|
||||||
|
const connected = await wsManager.connect(token)
|
||||||
|
if (connected) {
|
||||||
|
console.log('✅ WebSocket连接初始化成功')
|
||||||
|
|
||||||
|
// 添加连接状态监听
|
||||||
|
wsManager.addEventListener('onClose', () => {
|
||||||
|
console.log('❌ WebSocket连接已断开')
|
||||||
|
})
|
||||||
|
|
||||||
|
wsManager.addEventListener('onError', (error) => {
|
||||||
|
console.error('🚨 WebSocket连接错误:', error)
|
||||||
|
})
|
||||||
|
|
||||||
|
wsManager.addEventListener('onOpen', () => {
|
||||||
|
console.log('🎉 WebSocket连接已建立')
|
||||||
|
})
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ WebSocket连接初始化失败,将使用HTTP降级方案')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('💥 WebSocket连接初始化异常:', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ import {
|
||||||
formatChatMessage,
|
formatChatMessage,
|
||||||
formatMessageTime
|
formatMessageTime
|
||||||
} from '@/http/api/assistant.js'
|
} from '@/http/api/assistant.js'
|
||||||
import { createPetAssistantSSE } from '@/utils/sse.js'
|
import { createPetAssistantWebSocket } from '@/utils/websocket.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -192,6 +192,9 @@ export default {
|
||||||
// 开始AI思考
|
// 开始AI思考
|
||||||
this.isThinking = true
|
this.isThinking = true
|
||||||
|
|
||||||
|
// 用于存储当前AI消息的引用
|
||||||
|
let currentAiMessage = null
|
||||||
|
|
||||||
// 确保DOM更新后再滚动到思考状态
|
// 确保DOM更新后再滚动到思考状态
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.forceScrollToBottom()
|
this.forceScrollToBottom()
|
||||||
|
|
@ -205,11 +208,14 @@ export default {
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 使用SSE流式响应
|
// 使用WebSocket流式响应
|
||||||
const response = await this.handleStreamResponse({
|
const response = await this.handleStreamResponse({
|
||||||
message: userInput,
|
message: userInput,
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
maxTokens: 1000
|
maxTokens: 1000
|
||||||
|
}, (aiMessage) => {
|
||||||
|
// 保存AI消息引用,用于fallback时更新
|
||||||
|
currentAiMessage = aiMessage
|
||||||
})
|
})
|
||||||
|
|
||||||
// 检查是否包含敏感词
|
// 检查是否包含敏感词
|
||||||
|
|
@ -235,17 +241,25 @@ export default {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (fallbackResponse && fallbackResponse.data) {
|
if (fallbackResponse && fallbackResponse.data) {
|
||||||
const aiMessage = {
|
// 更新已存在的AI消息,而不是创建新的
|
||||||
type: 'ai',
|
if (currentAiMessage) {
|
||||||
content: fallbackResponse.data.message || fallbackResponse.data.content || '抱歉,我现在无法回答您的问题。',
|
currentAiMessage.content = fallbackResponse.data.message || fallbackResponse.data.content || '抱歉,我现在无法回答您的问题。'
|
||||||
time: this.getCurrentTime(),
|
currentAiMessage.isSensitive = fallbackResponse.data.isSensitive || false
|
||||||
isSensitive: fallbackResponse.data.isSensitive || false,
|
currentAiMessage.tokenCount = fallbackResponse.data.tokenCount || 0
|
||||||
tokenCount: fallbackResponse.data.tokenCount || 0,
|
currentAiMessage.responseTime = fallbackResponse.data.responseTime || 0
|
||||||
responseTime: fallbackResponse.data.responseTime || 0
|
} else {
|
||||||
|
// 如果没有AI消息引用,创建新的(兜底逻辑)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
this.messageList.push(aiMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 检查敏感词
|
// 检查敏感词
|
||||||
if (fallbackResponse.data.isSensitive) {
|
if (fallbackResponse.data.isSensitive) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
|
|
@ -255,14 +269,13 @@ export default {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.messageList.push(aiMessage)
|
|
||||||
this.forceScrollToBottom()
|
this.forceScrollToBottom()
|
||||||
} else {
|
} else {
|
||||||
this.handleApiError('API响应格式异常')
|
this.handleApiError('API响应格式异常', currentAiMessage)
|
||||||
}
|
}
|
||||||
} catch (fallbackError) {
|
} catch (fallbackError) {
|
||||||
console.error('普通请求也失败:', fallbackError)
|
console.error('普通请求也失败:', fallbackError)
|
||||||
this.handleApiError(fallbackError.message || '网络请求失败')
|
this.handleApiError(fallbackError.message || '网络请求失败', currentAiMessage)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// 确保思考状态结束(防止异常情况)
|
// 确保思考状态结束(防止异常情况)
|
||||||
|
|
@ -273,17 +286,26 @@ export default {
|
||||||
/**
|
/**
|
||||||
* 处理API错误
|
* 处理API错误
|
||||||
* @param {string} errorMessage 错误信息
|
* @param {string} errorMessage 错误信息
|
||||||
|
* @param {Object} existingAiMessage 已存在的AI消息对象(可选)
|
||||||
*/
|
*/
|
||||||
handleApiError(errorMessage) {
|
handleApiError(errorMessage, existingAiMessage = null) {
|
||||||
const errorMessage_display = {
|
const errorContent = `抱歉,我遇到了一些问题:${errorMessage}\n\n请稍后再试,或者检查网络连接。`
|
||||||
type: 'ai',
|
|
||||||
content: `抱歉,我遇到了一些问题:${errorMessage}\n\n请稍后再试,或者检查网络连接。`,
|
if (existingAiMessage) {
|
||||||
time: this.getCurrentTime(),
|
// 更新已存在的AI消息
|
||||||
isError: true
|
existingAiMessage.content = errorContent
|
||||||
|
existingAiMessage.isError = true
|
||||||
|
} else {
|
||||||
|
// 创建新的错误消息(兜底逻辑)
|
||||||
|
const errorMessage_display = {
|
||||||
|
type: 'ai',
|
||||||
|
content: errorContent,
|
||||||
|
time: this.getCurrentTime(),
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
this.messageList.push(errorMessage_display)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 直接添加错误消息
|
|
||||||
this.messageList.push(errorMessage_display)
|
|
||||||
this.forceScrollToBottom()
|
this.forceScrollToBottom()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -291,28 +313,43 @@ export default {
|
||||||
* 加载聊天历史记录
|
* 加载聊天历史记录
|
||||||
*/
|
*/
|
||||||
async loadChatHistory() {
|
async loadChatHistory() {
|
||||||
|
console.log('开始加载聊天历史记录...')
|
||||||
try {
|
try {
|
||||||
const response = await getAssistantHistory({
|
const response = await getAssistantHistory({
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 50
|
pageSize: 20
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response && response.data && response.data.list && response.data.list.length > 0) {
|
console.log('历史记录API响应:', response)
|
||||||
|
|
||||||
|
if (response && Array.isArray(response) && response.length > 0) {
|
||||||
// 格式化并设置历史消息
|
// 格式化并设置历史消息
|
||||||
const historyMessages = response.data.list.map(msg => formatChatMessage(msg))
|
const historyMessages = response.map(msg => formatChatMessage(msg))
|
||||||
|
console.log('格式化后的历史消息:', historyMessages)
|
||||||
this.messageList = historyMessages
|
this.messageList = historyMessages
|
||||||
|
|
||||||
// 滚动到底部
|
// 滚动到底部
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.scrollToBottom()
|
this.scrollToBottom()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log('历史记录加载成功,共', historyMessages.length, '条消息')
|
||||||
} else {
|
} else {
|
||||||
// 没有历史记录,保持空列表
|
// 没有历史记录,保持空列表
|
||||||
|
console.log('没有历史记录或数据格式异常:', response)
|
||||||
this.messageList = []
|
this.messageList = []
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载历史记录失败:', error)
|
console.error('加载历史记录失败:', error)
|
||||||
// 静默处理,保持空列表
|
|
||||||
|
// 显示用户友好的错误提示
|
||||||
|
uni.showToast({
|
||||||
|
title: '加载历史记录失败',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
|
||||||
|
// 保持空列表
|
||||||
this.messageList = []
|
this.messageList = []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -360,11 +397,12 @@ export default {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理SSE流式响应
|
* 处理WebSocket流式响应
|
||||||
* @param {Object} messageData 消息数据
|
* @param {Object} messageData 消息数据
|
||||||
|
* @param {Function} onAiMessageCreated 创建AI消息后的回调函数
|
||||||
* @returns {Promise} 返回流式响应处理结果
|
* @returns {Promise} 返回流式响应处理结果
|
||||||
*/
|
*/
|
||||||
async handleStreamResponse(messageData) {
|
async handleStreamResponse(messageData, onAiMessageCreated) {
|
||||||
// 创建AI消息对象
|
// 创建AI消息对象
|
||||||
const aiMessage = {
|
const aiMessage = {
|
||||||
type: 'ai',
|
type: 'ai',
|
||||||
|
|
@ -378,14 +416,23 @@ export default {
|
||||||
// 添加到消息列表
|
// 添加到消息列表
|
||||||
this.messageList.push(aiMessage)
|
this.messageList.push(aiMessage)
|
||||||
|
|
||||||
// 使用SSE工具类处理流式响应
|
// 通知调用者AI消息已创建
|
||||||
return createPetAssistantSSE(messageData, {
|
if (onAiMessageCreated) {
|
||||||
|
onAiMessageCreated(aiMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用WebSocket工具类处理流式响应
|
||||||
|
return createPetAssistantWebSocket(messageData, {
|
||||||
onChunk: (chunk) => {
|
onChunk: (chunk) => {
|
||||||
// 实时更新消息内容
|
console.log('收到WebSocket流式数据块:', chunk)
|
||||||
|
// 实时更新消息内容,实现打字机效果
|
||||||
aiMessage.content = chunk.totalContent
|
aiMessage.content = chunk.totalContent
|
||||||
|
// 强制触发视图更新
|
||||||
|
this.$forceUpdate()
|
||||||
this.forceScrollToBottom()
|
this.forceScrollToBottom()
|
||||||
},
|
},
|
||||||
onComplete: (result) => {
|
onComplete: (result) => {
|
||||||
|
console.log('WebSocket流式响应完成:', result)
|
||||||
// 更新消息的元数据
|
// 更新消息的元数据
|
||||||
if (result.data.isSensitive !== undefined) {
|
if (result.data.isSensitive !== undefined) {
|
||||||
aiMessage.isSensitive = result.data.isSensitive
|
aiMessage.isSensitive = result.data.isSensitive
|
||||||
|
|
@ -396,9 +443,17 @@ export default {
|
||||||
if (result.data.responseTime !== undefined) {
|
if (result.data.responseTime !== undefined) {
|
||||||
aiMessage.responseTime = result.data.responseTime
|
aiMessage.responseTime = result.data.responseTime
|
||||||
}
|
}
|
||||||
|
// 强制触发视图更新
|
||||||
|
this.$forceUpdate()
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.error('SSE流式响应失败:', error)
|
console.error('WebSocket流式响应失败:', error)
|
||||||
|
// 显示错误提示
|
||||||
|
uni.showToast({
|
||||||
|
title: '消息发送失败',
|
||||||
|
icon: 'error',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,141 +0,0 @@
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
}
|
|
||||||
|
|
@ -1,216 +0,0 @@
|
||||||
# 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)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
258
utils/sse.js
258
utils/sse.js
|
|
@ -1,258 +0,0 @@
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,780 @@
|
||||||
|
/**
|
||||||
|
* WebSocket连接管理工具类
|
||||||
|
* 提供WebSocket连接的建立、管理、自动重连等核心功能
|
||||||
|
* 基于uni-app的uni.connectSocket API实现
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { HTTP_CONFIG } from '../http/config/config.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket连接状态枚举
|
||||||
|
*/
|
||||||
|
export const WS_STATUS = {
|
||||||
|
DISCONNECTED: 'disconnected', // 未连接
|
||||||
|
CONNECTING: 'connecting', // 连接中
|
||||||
|
CONNECTED: 'connected', // 已连接
|
||||||
|
RECONNECTING: 'reconnecting', // 重连中
|
||||||
|
FAILED: 'failed' // 连接失败
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket消息类型枚举
|
||||||
|
*/
|
||||||
|
export const WS_MESSAGE_TYPE = {
|
||||||
|
CHAT: 'chat', // 聊天消息
|
||||||
|
CHUNK: 'chunk', // 流式数据块
|
||||||
|
COMPLETE: 'complete', // 完成标记
|
||||||
|
ERROR: 'error', // 错误消息
|
||||||
|
PING: 'ping', // 心跳检测
|
||||||
|
PONG: 'pong' // 心跳响应
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket管理器类 - 单例模式
|
||||||
|
*/
|
||||||
|
class WebSocketManager {
|
||||||
|
constructor() {
|
||||||
|
// 连接实例
|
||||||
|
this.ws = null
|
||||||
|
|
||||||
|
// 连接状态
|
||||||
|
this.status = WS_STATUS.DISCONNECTED
|
||||||
|
|
||||||
|
// 重连配置
|
||||||
|
this.reconnectAttempts = 0
|
||||||
|
this.maxReconnectAttempts = 5
|
||||||
|
this.reconnectDelay = 1000 // 初始重连延迟1秒
|
||||||
|
this.maxReconnectDelay = 30000 // 最大重连延迟30秒
|
||||||
|
this.reconnectTimer = null
|
||||||
|
|
||||||
|
// 心跳配置
|
||||||
|
this.heartbeatInterval = 30000 // 30秒心跳间隔
|
||||||
|
this.heartbeatTimer = null
|
||||||
|
this.heartbeatTimeoutTimer = null
|
||||||
|
|
||||||
|
// 连接配置
|
||||||
|
this.wsUrl = ''
|
||||||
|
this.token = ''
|
||||||
|
|
||||||
|
// 事件回调
|
||||||
|
this.eventCallbacks = {
|
||||||
|
onOpen: [],
|
||||||
|
onMessage: [],
|
||||||
|
onClose: [],
|
||||||
|
onError: []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 消息处理
|
||||||
|
this.messageCallbacks = new Map() // 存储消息回调
|
||||||
|
this.accumulatedContent = '' // 累积的消息内容
|
||||||
|
this.currentMessageId = null // 当前消息ID
|
||||||
|
|
||||||
|
console.log('WebSocketManager实例已创建')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单例实例
|
||||||
|
*/
|
||||||
|
static getInstance() {
|
||||||
|
if (!WebSocketManager.instance) {
|
||||||
|
WebSocketManager.instance = new WebSocketManager()
|
||||||
|
}
|
||||||
|
return WebSocketManager.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建WebSocket连接URL
|
||||||
|
* @param {string} token 用户token
|
||||||
|
* @returns {string} WebSocket URL
|
||||||
|
*/
|
||||||
|
buildWebSocketUrl(token) {
|
||||||
|
// 将HTTP URL转换为WebSocket URL
|
||||||
|
const baseURL = HTTP_CONFIG.baseURL
|
||||||
|
let wsBaseURL
|
||||||
|
|
||||||
|
if (baseURL.startsWith('https://')) {
|
||||||
|
wsBaseURL = baseURL.replace('https://', 'wss://')
|
||||||
|
} else if (baseURL.startsWith('http://')) {
|
||||||
|
wsBaseURL = baseURL.replace('http://', 'ws://')
|
||||||
|
} else {
|
||||||
|
// 默认使用ws://
|
||||||
|
wsBaseURL = `ws://${baseURL}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建WebSocket端点URL,携带token进行鉴权
|
||||||
|
return `${wsBaseURL}/user/ws?token=${encodeURIComponent(token)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 建立WebSocket连接
|
||||||
|
* @param {string} token 用户token
|
||||||
|
* @returns {Promise<boolean>} 连接是否成功
|
||||||
|
*/
|
||||||
|
async connect(token) {
|
||||||
|
if (!token) {
|
||||||
|
console.error('WebSocket连接失败:缺少token')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果已经连接,直接返回
|
||||||
|
if (this.status === WS_STATUS.CONNECTED) {
|
||||||
|
console.log('WebSocket已连接,无需重复连接')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果正在连接,等待连接完成
|
||||||
|
if (this.status === WS_STATUS.CONNECTING) {
|
||||||
|
console.log('WebSocket正在连接中,等待连接完成')
|
||||||
|
return this.waitForConnection()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.token = token
|
||||||
|
this.wsUrl = this.buildWebSocketUrl(token)
|
||||||
|
this.status = WS_STATUS.CONNECTING
|
||||||
|
|
||||||
|
console.log('开始建立WebSocket连接:', this.wsUrl)
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
try {
|
||||||
|
// 创建WebSocket连接
|
||||||
|
this.ws = uni.connectSocket({
|
||||||
|
url: this.wsUrl,
|
||||||
|
protocols: ['chat'] // 指定协议
|
||||||
|
})
|
||||||
|
|
||||||
|
// 设置事件监听器
|
||||||
|
this.setupEventListeners(resolve)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('WebSocket连接创建失败:', error)
|
||||||
|
this.status = WS_STATUS.FAILED
|
||||||
|
resolve(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等待连接完成
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
waitForConnection() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const checkConnection = () => {
|
||||||
|
if (this.status === WS_STATUS.CONNECTED) {
|
||||||
|
resolve(true)
|
||||||
|
} else if (this.status === WS_STATUS.FAILED || this.status === WS_STATUS.DISCONNECTED) {
|
||||||
|
resolve(false)
|
||||||
|
} else {
|
||||||
|
setTimeout(checkConnection, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkConnection()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置WebSocket事件监听器
|
||||||
|
* @param {Function} connectResolve 连接Promise的resolve函数
|
||||||
|
*/
|
||||||
|
setupEventListeners(connectResolve) {
|
||||||
|
if (!this.ws) return
|
||||||
|
|
||||||
|
// 连接打开事件
|
||||||
|
this.ws.onOpen(() => {
|
||||||
|
console.log('WebSocket连接已建立')
|
||||||
|
this.status = WS_STATUS.CONNECTED
|
||||||
|
this.reconnectAttempts = 0 // 重置重连次数
|
||||||
|
|
||||||
|
// 启动心跳检测
|
||||||
|
this.startHeartbeat()
|
||||||
|
|
||||||
|
// 触发连接成功回调
|
||||||
|
this.triggerCallbacks('onOpen')
|
||||||
|
|
||||||
|
if (connectResolve) {
|
||||||
|
connectResolve(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 消息接收事件
|
||||||
|
this.ws.onMessage((event) => {
|
||||||
|
console.log('WebSocket收到消息:', event.data)
|
||||||
|
this.handleMessage(event.data)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 连接关闭事件
|
||||||
|
this.ws.onClose((event) => {
|
||||||
|
console.log('WebSocket连接已关闭:', event)
|
||||||
|
this.status = WS_STATUS.DISCONNECTED
|
||||||
|
|
||||||
|
// 停止心跳检测
|
||||||
|
this.stopHeartbeat()
|
||||||
|
|
||||||
|
// 触发关闭回调
|
||||||
|
this.triggerCallbacks('onClose', event)
|
||||||
|
|
||||||
|
// 如果不是主动关闭,尝试重连
|
||||||
|
if (event.code !== 1000) { // 1000表示正常关闭
|
||||||
|
this.attemptReconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectResolve) {
|
||||||
|
connectResolve(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 连接错误事件
|
||||||
|
this.ws.onError((error) => {
|
||||||
|
console.error('WebSocket连接错误:', error)
|
||||||
|
this.status = WS_STATUS.FAILED
|
||||||
|
|
||||||
|
// 停止心跳检测
|
||||||
|
this.stopHeartbeat()
|
||||||
|
|
||||||
|
// 触发错误回调
|
||||||
|
this.triggerCallbacks('onError', error)
|
||||||
|
|
||||||
|
if (connectResolve) {
|
||||||
|
connectResolve(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动心跳检测
|
||||||
|
*/
|
||||||
|
startHeartbeat() {
|
||||||
|
this.stopHeartbeat() // 先停止之前的心跳
|
||||||
|
|
||||||
|
this.heartbeatTimer = setInterval(() => {
|
||||||
|
if (this.status === WS_STATUS.CONNECTED) {
|
||||||
|
// 发送心跳消息
|
||||||
|
this.sendRawMessage({
|
||||||
|
type: WS_MESSAGE_TYPE.PING,
|
||||||
|
timestamp: Date.now()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 设置心跳超时检测
|
||||||
|
this.heartbeatTimeoutTimer = setTimeout(() => {
|
||||||
|
console.warn('心跳超时,连接可能已断开')
|
||||||
|
this.disconnect()
|
||||||
|
this.attemptReconnect()
|
||||||
|
}, 10000) // 10秒超时
|
||||||
|
}
|
||||||
|
}, this.heartbeatInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止心跳检测
|
||||||
|
*/
|
||||||
|
stopHeartbeat() {
|
||||||
|
if (this.heartbeatTimer) {
|
||||||
|
clearInterval(this.heartbeatTimer)
|
||||||
|
this.heartbeatTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.heartbeatTimeoutTimer) {
|
||||||
|
clearTimeout(this.heartbeatTimeoutTimer)
|
||||||
|
this.heartbeatTimeoutTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 尝试重连
|
||||||
|
*/
|
||||||
|
attemptReconnect() {
|
||||||
|
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||||
|
console.error('WebSocket重连次数已达上限,停止重连')
|
||||||
|
this.status = WS_STATUS.FAILED
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.status === WS_STATUS.RECONNECTING) {
|
||||||
|
console.log('WebSocket正在重连中,跳过本次重连')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.status = WS_STATUS.RECONNECTING
|
||||||
|
this.reconnectAttempts++
|
||||||
|
|
||||||
|
// 计算重连延迟(指数退避)
|
||||||
|
const delay = Math.min(
|
||||||
|
this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1),
|
||||||
|
this.maxReconnectDelay
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log(`WebSocket第${this.reconnectAttempts}次重连,延迟${delay}ms`)
|
||||||
|
|
||||||
|
this.reconnectTimer = setTimeout(() => {
|
||||||
|
this.connect(this.token)
|
||||||
|
}, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 断开WebSocket连接
|
||||||
|
*/
|
||||||
|
disconnect() {
|
||||||
|
console.log('主动断开WebSocket连接')
|
||||||
|
|
||||||
|
// 停止心跳检测
|
||||||
|
this.stopHeartbeat()
|
||||||
|
|
||||||
|
// 清除重连定时器
|
||||||
|
if (this.reconnectTimer) {
|
||||||
|
clearTimeout(this.reconnectTimer)
|
||||||
|
this.reconnectTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭连接
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.close({
|
||||||
|
code: 1000,
|
||||||
|
reason: '主动断开连接'
|
||||||
|
})
|
||||||
|
this.ws = null
|
||||||
|
}
|
||||||
|
|
||||||
|
this.status = WS_STATUS.DISCONNECTED
|
||||||
|
this.reconnectAttempts = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查连接状态
|
||||||
|
* @returns {boolean} 是否已连接
|
||||||
|
*/
|
||||||
|
isConnected() {
|
||||||
|
return this.status === WS_STATUS.CONNECTED
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前连接状态
|
||||||
|
* @returns {string} 连接状态
|
||||||
|
*/
|
||||||
|
getStatus() {
|
||||||
|
return this.status
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新token并重新连接
|
||||||
|
* @param {string} newToken 新的token
|
||||||
|
*/
|
||||||
|
async updateToken(newToken) {
|
||||||
|
if (this.token === newToken) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('更新WebSocket token并重新连接')
|
||||||
|
this.disconnect()
|
||||||
|
await this.connect(newToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送原始消息
|
||||||
|
* @param {Object} message 消息对象
|
||||||
|
*/
|
||||||
|
sendRawMessage(message) {
|
||||||
|
if (!this.isConnected()) {
|
||||||
|
console.warn('WebSocket未连接,无法发送消息')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const messageStr = JSON.stringify(message)
|
||||||
|
this.ws.send({
|
||||||
|
data: messageStr
|
||||||
|
})
|
||||||
|
console.log('WebSocket发送消息:', message)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('WebSocket发送消息失败:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理接收到的消息
|
||||||
|
* @param {string} rawData 原始消息数据
|
||||||
|
*/
|
||||||
|
handleMessage(rawData) {
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(rawData)
|
||||||
|
console.log('WebSocket解析消息:', message)
|
||||||
|
|
||||||
|
// 处理不同类型的消息
|
||||||
|
switch (message.type) {
|
||||||
|
case WS_MESSAGE_TYPE.PONG:
|
||||||
|
// 收到心跳响应,清除超时定时器
|
||||||
|
if (this.heartbeatTimeoutTimer) {
|
||||||
|
clearTimeout(this.heartbeatTimeoutTimer)
|
||||||
|
this.heartbeatTimeoutTimer = null
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case WS_MESSAGE_TYPE.CHUNK:
|
||||||
|
this.handleChunkMessage(message)
|
||||||
|
break
|
||||||
|
|
||||||
|
case WS_MESSAGE_TYPE.COMPLETE:
|
||||||
|
this.handleCompleteMessage(message)
|
||||||
|
break
|
||||||
|
|
||||||
|
case WS_MESSAGE_TYPE.ERROR:
|
||||||
|
this.handleErrorMessage(message)
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log('收到未知类型消息:', message)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发通用消息回调
|
||||||
|
this.triggerCallbacks('onMessage', message)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('WebSocket消息解析失败:', error, rawData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理流式数据块消息
|
||||||
|
* @param {Object} message 消息对象
|
||||||
|
*/
|
||||||
|
handleChunkMessage(message) {
|
||||||
|
const { data } = message
|
||||||
|
if (!data || !data.delta) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 累积内容
|
||||||
|
this.accumulatedContent += data.delta
|
||||||
|
|
||||||
|
// 获取对应的回调函数
|
||||||
|
const messageId = data.sessionId || data.messageId || 'default'
|
||||||
|
const callbacks = this.messageCallbacks.get(messageId)
|
||||||
|
|
||||||
|
if (callbacks && callbacks.onChunk) {
|
||||||
|
callbacks.onChunk({
|
||||||
|
content: data.delta,
|
||||||
|
totalContent: this.accumulatedContent,
|
||||||
|
chunk: message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理完成消息
|
||||||
|
* @param {Object} message 消息对象
|
||||||
|
*/
|
||||||
|
handleCompleteMessage(message) {
|
||||||
|
const { data } = message
|
||||||
|
const messageId = data.sessionId || data.messageId || 'default'
|
||||||
|
const callbacks = this.messageCallbacks.get(messageId)
|
||||||
|
|
||||||
|
if (callbacks && callbacks.onComplete) {
|
||||||
|
const result = {
|
||||||
|
data: {
|
||||||
|
message: this.accumulatedContent,
|
||||||
|
isSensitive: data.isSensitive,
|
||||||
|
tokenCount: data.tokenCount,
|
||||||
|
responseTime: data.responseTime,
|
||||||
|
...data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callbacks.onComplete(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理回调和累积内容
|
||||||
|
this.messageCallbacks.delete(messageId)
|
||||||
|
this.accumulatedContent = ''
|
||||||
|
this.currentMessageId = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理错误消息
|
||||||
|
* @param {Object} message 消息对象
|
||||||
|
*/
|
||||||
|
handleErrorMessage(message) {
|
||||||
|
const { data } = message
|
||||||
|
const messageId = data.sessionId || data.messageId || 'default'
|
||||||
|
const callbacks = this.messageCallbacks.get(messageId)
|
||||||
|
|
||||||
|
if (callbacks && callbacks.onError) {
|
||||||
|
callbacks.onError(new Error(data.error || '服务器返回错误'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理回调和累积内容
|
||||||
|
this.messageCallbacks.delete(messageId)
|
||||||
|
this.accumulatedContent = ''
|
||||||
|
this.currentMessageId = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送聊天消息
|
||||||
|
* @param {Object} messageData 消息数据
|
||||||
|
* @param {Object} callbacks 回调函数 {onChunk, onComplete, onError}
|
||||||
|
* @returns {Promise} 发送结果
|
||||||
|
*/
|
||||||
|
sendMessage(messageData, callbacks = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!this.isConnected()) {
|
||||||
|
const error = new Error('WebSocket未连接')
|
||||||
|
if (callbacks.onError) {
|
||||||
|
callbacks.onError(error)
|
||||||
|
}
|
||||||
|
reject(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成消息ID
|
||||||
|
const messageId = messageData.sessionId || `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||||
|
|
||||||
|
// 存储回调函数
|
||||||
|
this.messageCallbacks.set(messageId, {
|
||||||
|
...callbacks,
|
||||||
|
resolve,
|
||||||
|
reject
|
||||||
|
})
|
||||||
|
|
||||||
|
// 重置累积内容
|
||||||
|
this.accumulatedContent = ''
|
||||||
|
this.currentMessageId = messageId
|
||||||
|
|
||||||
|
// 构建消息
|
||||||
|
const message = {
|
||||||
|
type: WS_MESSAGE_TYPE.CHAT,
|
||||||
|
data: {
|
||||||
|
...messageData,
|
||||||
|
messageId
|
||||||
|
},
|
||||||
|
timestamp: Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送消息
|
||||||
|
const success = this.sendRawMessage(message)
|
||||||
|
if (!success) {
|
||||||
|
const error = new Error('消息发送失败')
|
||||||
|
this.messageCallbacks.delete(messageId)
|
||||||
|
if (callbacks.onError) {
|
||||||
|
callbacks.onError(error)
|
||||||
|
}
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加事件监听器
|
||||||
|
* @param {string} event 事件名称
|
||||||
|
* @param {Function} callback 回调函数
|
||||||
|
*/
|
||||||
|
addEventListener(event, callback) {
|
||||||
|
if (this.eventCallbacks[event]) {
|
||||||
|
this.eventCallbacks[event].push(callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除事件监听器
|
||||||
|
* @param {string} event 事件名称
|
||||||
|
* @param {Function} callback 回调函数
|
||||||
|
*/
|
||||||
|
removeEventListener(event, callback) {
|
||||||
|
if (this.eventCallbacks[event]) {
|
||||||
|
const index = this.eventCallbacks[event].indexOf(callback)
|
||||||
|
if (index > -1) {
|
||||||
|
this.eventCallbacks[event].splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发事件回调
|
||||||
|
* @param {string} event 事件名称
|
||||||
|
* @param {*} data 事件数据
|
||||||
|
*/
|
||||||
|
triggerCallbacks(event, data) {
|
||||||
|
if (this.eventCallbacks[event]) {
|
||||||
|
this.eventCallbacks[event].forEach(callback => {
|
||||||
|
try {
|
||||||
|
callback(data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`事件回调执行失败 [${event}]:`, error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否应该使用降级方案
|
||||||
|
* @returns {boolean} 是否使用降级
|
||||||
|
*/
|
||||||
|
shouldUseFallback() {
|
||||||
|
// 如果重连次数过多,使用降级方案
|
||||||
|
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果连接状态为失败,使用降级方案
|
||||||
|
if (this.status === WS_STATUS.FAILED) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置连接状态
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
this.disconnect()
|
||||||
|
this.reconnectAttempts = 0
|
||||||
|
this.status = WS_STATUS.DISCONNECTED
|
||||||
|
this.messageCallbacks.clear()
|
||||||
|
this.accumulatedContent = ''
|
||||||
|
this.currentMessageId = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取连接信息
|
||||||
|
* @returns {Object} 连接信息
|
||||||
|
*/
|
||||||
|
getConnectionInfo() {
|
||||||
|
return {
|
||||||
|
status: this.status,
|
||||||
|
url: this.wsUrl,
|
||||||
|
reconnectAttempts: this.reconnectAttempts,
|
||||||
|
maxReconnectAttempts: this.maxReconnectAttempts,
|
||||||
|
hasToken: !!this.token,
|
||||||
|
activeCallbacks: this.messageCallbacks.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建宠物助手专用WebSocket接口
|
||||||
|
* @param {Object} messageData 消息数据
|
||||||
|
* @param {Object} callbacks 回调函数 {onChunk, onComplete, onError}
|
||||||
|
* @returns {Promise} WebSocket请求Promise
|
||||||
|
*/
|
||||||
|
export function createPetAssistantWebSocket(messageData, callbacks = {}) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const wsManager = WebSocketManager.getInstance()
|
||||||
|
|
||||||
|
// 检查是否应该使用降级方案
|
||||||
|
if (wsManager.shouldUseFallback()) {
|
||||||
|
throw new Error('WebSocket不可用,使用降级方案')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查连接状态,必要时建立连接
|
||||||
|
if (!wsManager.isConnected()) {
|
||||||
|
const token = uni.getStorageSync(HTTP_CONFIG.storageKeys.token)
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('缺少认证token')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('WebSocket未连接,正在建立连接...')
|
||||||
|
const connected = await wsManager.connect(token)
|
||||||
|
if (!connected) {
|
||||||
|
throw new Error('WebSocket连接失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送消息并处理回调
|
||||||
|
await wsManager.sendMessage(messageData, {
|
||||||
|
onChunk: callbacks.onChunk,
|
||||||
|
onComplete: (result) => {
|
||||||
|
if (callbacks.onComplete) {
|
||||||
|
callbacks.onComplete(result)
|
||||||
|
}
|
||||||
|
resolve(result)
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
if (callbacks.onError) {
|
||||||
|
callbacks.onError(error)
|
||||||
|
}
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('WebSocket请求失败,尝试降级方案:', error)
|
||||||
|
|
||||||
|
// 降级到HTTP API
|
||||||
|
try {
|
||||||
|
const { askPetAssistant } = await import('../http/api/assistant.js')
|
||||||
|
const result = await askPetAssistant(messageData)
|
||||||
|
|
||||||
|
// 模拟流式效果
|
||||||
|
if (callbacks.onChunk && result.message) {
|
||||||
|
simulateStreamingEffect(result.message, callbacks.onChunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发完成回调
|
||||||
|
const completeResult = {
|
||||||
|
data: {
|
||||||
|
message: result.message || result.content || '',
|
||||||
|
isSensitive: result.isSensitive || false,
|
||||||
|
tokenCount: result.tokenCount || 0,
|
||||||
|
responseTime: result.responseTime || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callbacks.onComplete) {
|
||||||
|
callbacks.onComplete(completeResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(completeResult)
|
||||||
|
|
||||||
|
} catch (fallbackError) {
|
||||||
|
console.error('降级方案也失败:', fallbackError)
|
||||||
|
if (callbacks.onError) {
|
||||||
|
callbacks.onError(fallbackError)
|
||||||
|
}
|
||||||
|
reject(fallbackError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模拟流式效果
|
||||||
|
* @param {string} content 完整内容
|
||||||
|
* @param {Function} onChunk 数据块回调
|
||||||
|
*/
|
||||||
|
function simulateStreamingEffect(content, onChunk) {
|
||||||
|
if (!content || !onChunk) return
|
||||||
|
|
||||||
|
let index = 0
|
||||||
|
let accumulatedContent = ''
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (index >= content.length) {
|
||||||
|
clearInterval(interval)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每次发送1-3个字符,模拟真实的流式效果
|
||||||
|
const chunkSize = Math.min(Math.floor(Math.random() * 3) + 1, content.length - index)
|
||||||
|
const chunk = content.substr(index, chunkSize)
|
||||||
|
accumulatedContent += chunk
|
||||||
|
index += chunkSize
|
||||||
|
|
||||||
|
onChunk({
|
||||||
|
content: chunk,
|
||||||
|
totalContent: accumulatedContent,
|
||||||
|
chunk: { data: { delta: chunk } }
|
||||||
|
})
|
||||||
|
}, 50) // 50ms间隔,模拟打字机效果
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出WebSocket管理器类和相关常量
|
||||||
|
export { WebSocketManager, WS_STATUS, WS_MESSAGE_TYPE }
|
||||||
|
|
||||||
|
// 默认导出
|
||||||
|
export default {
|
||||||
|
WebSocketManager,
|
||||||
|
createPetAssistantWebSocket,
|
||||||
|
WS_STATUS,
|
||||||
|
WS_MESSAGE_TYPE
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue