|
@@ -183,59 +183,124 @@ const sendMessage = async () => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// AI 对话接口 (SSE 流式输出)
|
|
|
|
|
|
|
+// 打字机效果显示文本
|
|
|
|
|
+const typeWriterEffect = async (index: number, text: string, delay: number = 50) => {
|
|
|
|
|
+ for (let i = 0; i < text.length; i++) {
|
|
|
|
|
+ if (messages.value[index]) {
|
|
|
|
|
+ messages.value[index].content += text[i]
|
|
|
|
|
+ scrollToBottom()
|
|
|
|
|
+ }
|
|
|
|
|
+ await new Promise(resolve => setTimeout(resolve, delay))
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// AI 对话接口 (同时支持 SSE 和普通 JSON)
|
|
|
const sendChatMessage = async (question: string): Promise<void> => {
|
|
const sendChatMessage = async (question: string): Promise<void> => {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
|
|
|
|
+ // 先添加一个空的 AI 消息
|
|
|
|
|
+ const aiMessageIndex = messages.value.length
|
|
|
|
|
+ messages.value.push({
|
|
|
|
|
+ role: 'assistant',
|
|
|
|
|
+ content: '',
|
|
|
|
|
+ time: new Date().toLocaleString()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 先尝试普通 POST 请求(适用于代理模式,后端返回 JSON)
|
|
|
|
|
+ const response = await fetch(`/api/upms/ai/chat`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
|
|
+ },
|
|
|
|
|
+ body: JSON.stringify({ question })
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 检查响应类型
|
|
|
|
|
+ const contentType = response.headers.get('content-type') || ''
|
|
|
|
|
+
|
|
|
|
|
+ if (contentType.includes('text/event-stream')) {
|
|
|
|
|
+ // SSE 流式响应(Mock 模式)
|
|
|
|
|
+ await handleSSEResponse(response, aiMessageIndex)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // JSON 响应(代理模式)
|
|
|
|
|
+ const data = await response.json()
|
|
|
|
|
+ const reply = data.data?.reply || data.reply || '暂无回复'
|
|
|
|
|
+ // 使用打字机效果显示
|
|
|
|
|
+ await typeWriterEffect(aiMessageIndex, reply, 30)
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('AI 请求失败:', error)
|
|
|
|
|
+ // 尝试 SSE 方式(兼容旧版 Mock)
|
|
|
try {
|
|
try {
|
|
|
- // 先添加一个空的 AI 消息
|
|
|
|
|
- const aiMessageIndex = messages.value.length
|
|
|
|
|
- messages.value.push({
|
|
|
|
|
- role: 'assistant',
|
|
|
|
|
- content: '',
|
|
|
|
|
- time: new Date().toLocaleString()
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- // 创建 SSE 连接
|
|
|
|
|
- const eventSource = new EventSource(`/api/upms/ai/chat?question=${encodeURIComponent(question)}`)
|
|
|
|
|
-
|
|
|
|
|
- eventSource.onmessage = (event) => {
|
|
|
|
|
- try {
|
|
|
|
|
- const data = JSON.parse(event.data)
|
|
|
|
|
|
|
+ await handleSSEFallback(question, aiMessageIndex)
|
|
|
|
|
+ } catch (sseError) {
|
|
|
|
|
+ console.error('SSE 也失败了:', sseError)
|
|
|
|
|
+ if (messages.value[aiMessageIndex]) {
|
|
|
|
|
+ messages.value[aiMessageIndex].content = '抱歉,AI 服务暂时不可用,请稍后再试。'
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- // 追加内容到 AI 消息
|
|
|
|
|
- if (messages.value[aiMessageIndex]) {
|
|
|
|
|
- messages.value[aiMessageIndex].content += data.content
|
|
|
|
|
- }
|
|
|
|
|
|
|
+// 处理 SSE 流响应
|
|
|
|
|
+const handleSSEResponse = async (response: Response, index: number): Promise<void> => {
|
|
|
|
|
+ const reader = response.body?.getReader()
|
|
|
|
|
+ if (!reader) throw new Error('无法读取响应流')
|
|
|
|
|
|
|
|
- // 滚动到底部
|
|
|
|
|
- scrollToBottom()
|
|
|
|
|
|
|
+ const decoder = new TextDecoder()
|
|
|
|
|
+ let buffer = ''
|
|
|
|
|
|
|
|
- // 如果完成了,关闭连接
|
|
|
|
|
- if (data.done) {
|
|
|
|
|
- eventSource.close()
|
|
|
|
|
- resolve()
|
|
|
|
|
|
|
+ while (true) {
|
|
|
|
|
+ const { done, value } = await reader.read()
|
|
|
|
|
+ if (done) break
|
|
|
|
|
+
|
|
|
|
|
+ buffer += decoder.decode(value, { stream: true })
|
|
|
|
|
+ const lines = buffer.split('\n')
|
|
|
|
|
+ buffer = lines.pop() || ''
|
|
|
|
|
+
|
|
|
|
|
+ for (const line of lines) {
|
|
|
|
|
+ if (line.startsWith('data: ')) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const data = JSON.parse(line.slice(6))
|
|
|
|
|
+ if (messages.value[index]) {
|
|
|
|
|
+ messages.value[index].content += data.content
|
|
|
|
|
+ scrollToBottom()
|
|
|
}
|
|
}
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error('解析 SSE 数据失败:', error)
|
|
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ // 忽略解析错误
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- eventSource.onerror = (error) => {
|
|
|
|
|
- console.error('SSE 连接错误:', error)
|
|
|
|
|
- eventSource.close()
|
|
|
|
|
|
|
+// SSE 降级方案(使用 EventSource)
|
|
|
|
|
+const handleSSEFallback = (question: string, index: number): Promise<void> => {
|
|
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
|
|
+ const eventSource = new EventSource(`/api/upms/ai/chat?question=${encodeURIComponent(question)}`)
|
|
|
|
|
|
|
|
- // 如果有内容,则认为是正常结束
|
|
|
|
|
- if (messages.value[aiMessageIndex]?.content) {
|
|
|
|
|
|
|
+ eventSource.onmessage = (event) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const data = JSON.parse(event.data)
|
|
|
|
|
+ if (messages.value[index]) {
|
|
|
|
|
+ messages.value[index].content += data.content
|
|
|
|
|
+ scrollToBottom()
|
|
|
|
|
+ }
|
|
|
|
|
+ if (data.done) {
|
|
|
|
|
+ eventSource.close()
|
|
|
resolve()
|
|
resolve()
|
|
|
- } else {
|
|
|
|
|
- // 如果没有内容,显示错误信息
|
|
|
|
|
- messages.value[aiMessageIndex].content = '抱歉,AI 服务暂时不可用,请稍后再试。'
|
|
|
|
|
- reject(error)
|
|
|
|
|
}
|
|
}
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('解析 SSE 数据失败:', error)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ eventSource.onerror = (error) => {
|
|
|
|
|
+ eventSource.close()
|
|
|
|
|
+ if (messages.value[index]?.content) {
|
|
|
|
|
+ resolve()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ reject(error)
|
|
|
}
|
|
}
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error('创建 SSE 连接失败:', error)
|
|
|
|
|
- reject(error)
|
|
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|