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
+120
View File
@@ -0,0 +1,120 @@
import { getEnvValue } from '../config/env.js'
const API_BASE_URL = getEnvValue('API_BASE_URL')
const getAuthHeader = () => {
const token = uni.getStorageSync('access_token')
return token ? { Authorization: `Bearer ${token}` } : {}
}
const decodeChunk = (arrayBuffer) => {
if (typeof TextDecoder !== 'undefined') {
return new TextDecoder('utf-8').decode(arrayBuffer)
}
const bytes = new Uint8Array(arrayBuffer)
let binary = ''
bytes.forEach(byte => {
binary += String.fromCharCode(byte)
})
try {
return decodeURIComponent(escape(binary))
} catch (error) {
return binary
}
}
const parseSseFrame = (frame) => {
const event = { type: 'message', data: '' }
frame.split(/\r?\n/).forEach((line) => {
if (line.startsWith('event:')) event.type = line.slice(6).trim()
if (line.startsWith('data:')) event.data += line.slice(5).trim()
})
if (!event.data) return null
try {
return JSON.parse(event.data)
} catch (error) {
return { type: event.type, content: event.data }
}
}
export const streamAiScene = ({ sceneCode, inputs = {}, onStart, onDelta, onDone, onError }) => {
let buffer = ''
let output = ''
let closed = false
return new Promise((resolve, reject) => {
const failStream = (message, event) => {
if (closed) return
closed = true
onError?.(message, event)
reject(new Error(message))
}
const requestTask = uni.request({
url: `${API_BASE_URL}/ai/runtime/stream`,
method: 'POST',
data: { sceneCode, inputs },
header: {
'Content-Type': 'application/json',
...getAuthHeader()
},
enableChunked: true,
timeout: 120000,
success: (res) => {
if (closed) return
if (res.statusCode >= 400) {
const message = res.data?.message || 'AI流式请求失败'
failStream(message)
return
}
if (typeof res.data === 'string' && res.data) {
consumeText(res.data, failStream)
}
if (buffer.trim()) {
consumeText('\n\n', failStream)
}
closed = true
resolve({ output })
},
fail: (error) => {
failStream(error.errMsg || 'AI流式请求失败')
}
})
requestTask?.onChunkReceived?.((res) => {
try {
consumeText(decodeChunk(res.data), failStream)
} catch (error) {
failStream(error.message || 'AI流式请求失败')
}
})
})
function consumeText(text, failStream) {
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') {
output += event.content || ''
onDelta?.(event.content || '', output, event)
} else if (event.type === 'done') {
onDone?.(event, output)
} else if (event.type === 'error') {
const message = event.message || event.code || 'AI流式请求失败'
failStream(message, event)
}
})
if (buffer.trim() && text === '') {
buffer = ''
}
}
}
export default {
streamAiScene
}
+9
View File
@@ -0,0 +1,9 @@
import { upload } from './request.js'
export const transcribeAudio = (filePath) => {
return upload('/asr/transcribe', filePath, {}, 'file')
}
export default {
transcribeAudio
}
+17 -2
View File
@@ -4,6 +4,7 @@
*/
import { get, post, put, del } from './request.js'
import { streamAiScene } from './aiRuntime.js'
export const getEventList = async () => {
const response = await get('/lifeEvent/list')
@@ -37,8 +38,22 @@ export const deleteEvent = async (id) => {
return response
}
export const assistEventWriting = async (eventData = {}) => {
return post('/lifeEvent/ai-assist', eventData)
export const assistEventWriting = async (eventData = {}, callbacks = {}) => {
const result = await streamAiScene({
sceneCode: 'life_healing',
inputs: {
mode: 'life_event_assist',
prompt: `请帮我优化这段人生事件记录,保留真实细节,让表达更清晰温柔。\n标题:${eventData.title || ''}\n时间:${eventData.time || eventData.eventDateText || ''}\n内容:${eventData.content || ''}`,
...eventData
},
...callbacks
})
return {
data: {
content: result.output,
tags: eventData.tags || []
}
}
}
export const chatAboutEvent = async (eventData = {}) => {