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:
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { upload } from './request.js'
|
||||
|
||||
export const transcribeAudio = (filePath) => {
|
||||
return upload('/asr/transcribe', filePath, {}, 'file')
|
||||
}
|
||||
|
||||
export default {
|
||||
transcribeAudio
|
||||
}
|
||||
@@ -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 = {}) => {
|
||||
|
||||
Reference in New Issue
Block a user