feat: AI 场景路由、ASR 服务及前后端全链路同步

- 新增 AI 场景路由控制器和管理接口
- 新增 ASR 语音识别服务及前后端集成
- 同步 AI Runtime 客户端到 Web/小程序/Life-Script
- 完善 AI 配置测试修复和管理后台路由配置
- 新增数据库迁移脚本

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 13:25:21 +08:00
parent d77090aa5e
commit 89fc42819d
72 changed files with 4584 additions and 383 deletions
+100
View File
@@ -0,0 +1,100 @@
import { envConfig } from '@/config/env'
export interface AiStreamEvent {
type: 'start' | 'delta' | 'done' | 'error' | string
content?: string
code?: string
message?: string
seq?: number
metadata?: Record<string, any>
timestamp?: number
}
export interface StreamAiSceneOptions {
sceneCode: string
inputs?: Record<string, any>
onStart?: (event: AiStreamEvent) => void
onDelta?: (delta: string, output: string, event: AiStreamEvent) => void
onDone?: (event: AiStreamEvent, output: string) => void
onError?: (message: string, event?: AiStreamEvent) => void
}
const parseSseFrame = (frame: string): AiStreamEvent | null => {
const parsed = { type: 'message', data: '' }
frame.split(/\r?\n/).forEach((line) => {
if (line.startsWith('event:')) parsed.type = line.slice(6).trim()
if (line.startsWith('data:')) parsed.data += line.slice(5).trim()
})
if (!parsed.data) return null
try {
return JSON.parse(parsed.data)
} catch {
return { type: parsed.type, content: parsed.data }
}
}
export const streamAiScene = async ({
sceneCode,
inputs = {},
onStart,
onDelta,
onDone,
onError
}: StreamAiSceneOptions): Promise<{ output: string }> => {
const token = localStorage.getItem('access_token')
const response = await fetch(`${envConfig.apiBaseUrl}/ai/runtime/stream`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {})
},
body: JSON.stringify({ sceneCode, inputs })
})
if (!response.ok || !response.body) {
const message = `AI流式请求失败(${response.status})`
onError?.(message)
throw new Error(message)
}
const reader = response.body.getReader()
const decoder = new TextDecoder('utf-8')
let buffer = ''
let output = ''
const consumeText = (text: string) => {
buffer += text
const frames = buffer.split(/\r?\n\r?\n/)
buffer = frames.pop() || ''
frames.forEach((frame) => {
const event = parseSseFrame(frame)
if (!event) return
if (event.type === 'start') {
onStart?.(event)
} else if (event.type === 'delta') {
const delta = event.content || ''
output += delta
onDelta?.(delta, output, event)
} else if (event.type === 'done') {
onDone?.(event, output)
} else if (event.type === 'error') {
const message = event.message || event.code || 'AI流式请求失败'
onError?.(message, event)
throw new Error(message)
}
})
}
while (true) {
const { value, done } = await reader.read()
if (done) break
consumeText(decoder.decode(value, { stream: true }))
}
consumeText(decoder.decode())
if (buffer.trim()) consumeText('\n\n')
return { output }
}
export default {
streamAiScene
}
+1 -1
View File
@@ -6,7 +6,7 @@ import { envConfig } from '@/config/env'
export interface WebSocketMessage {
messageId?: string
conversationId?: string
type: 'TEXT' | 'TYPING' | 'SYSTEM' | 'ERROR' | 'HEARTBEAT' | 'CONNECTION' | 'AI_THINKING'
type: 'TEXT' | 'TYPING' | 'SYSTEM' | 'ERROR' | 'HEARTBEAT' | 'CONNECTION' | 'AI_THINKING' | 'AI_STREAM_START' | 'AI_STREAM_DELTA' | 'AI_STREAM_DONE' | 'AI_STREAM_ERROR' | 'AI_STREAM_EVENT'
content: string
senderId: string
senderType: 'USER' | 'GUEST' | 'AI' | 'SYSTEM'
+62
View File
@@ -469,6 +469,60 @@ export const useChatStore = defineStore('chat', () => {
await processMessageQueue()
}
const ensureStreamingAiMessage = async (wsMessage: WebSocketMessage) => {
const messageId = wsMessage.messageId || `ai-stream-${Date.now()}`
let message = messages.value.find(m => m.id === messageId)
if (!message) {
await processMessageQueue()
message = messages.value.find(m => m.id === messageId)
}
if (!message) {
addMessage({
id: messageId,
content: '',
type: 'ai',
conversationId: wsMessage.conversationId || currentSession.value?.id,
timestamp: wsMessage.createTime || new Date().toISOString()
})
await processMessageQueue()
message = messages.value.find(m => m.id === messageId)
}
return message
}
const handleAiStreamMessage = async (wsMessage: WebSocketMessage) => {
const message = await ensureStreamingAiMessage(wsMessage)
if (!message) return
if (wsMessage.type === 'AI_STREAM_START') {
isTyping.value = true
message.status = 'sending'
return
}
if (wsMessage.type === 'AI_STREAM_DELTA') {
isTyping.value = false
message.content += wsMessage.content || ''
message.status = 'sending'
return
}
if (wsMessage.type === 'AI_STREAM_DONE') {
isTyping.value = false
message.status = 'sent'
return
}
if (wsMessage.type === 'AI_STREAM_ERROR') {
isTyping.value = false
message.status = 'failed'
message.error = wsMessage.data?.message || wsMessage.content || 'AI服务暂时不可用'
if (!message.content) {
message.content = message.error
}
}
}
// WebSocket消息处理 - 使用队列处理所有消息
let handleWebSocketMessage = async (wsMessage: WebSocketMessage) => {
console.log('收到WebSocket消息:', wsMessage.type, wsMessage.senderType, '消息ID:', wsMessage.messageId)
@@ -484,6 +538,14 @@ export const useChatStore = defineStore('chat', () => {
case 'AI_THINKING':
// AI正在思考 - 不修改响应式数据,避免竞态
console.log('⏳ AI正在思考中...')
isTyping.value = true
break
case 'AI_STREAM_START':
case 'AI_STREAM_DELTA':
case 'AI_STREAM_DONE':
case 'AI_STREAM_ERROR':
await handleAiStreamMessage(wsMessage)
break
case 'CONNECTION':