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
}