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
+35 -68
View File
@@ -1,78 +1,45 @@
/**
* AI 服务模块
* 封装 OpenRouter API 调用
*/
import { streamAiScene } from './aiRuntime';
const API_KEY = "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";
const BASE_URL = "https://openrouter.ai/api/v1/chat/completions";
/**
* 调用 AI API
* @param {string} prompt - 用户提示
* @param {string} systemMsg - 系统消息
* @returns {Promise<string>} AI 响应内容
*/
const fetchAI = async (prompt, systemMsg) => {
try {
const response = await fetch(BASE_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: "deepseek/deepseek-chat-v3-0324:free",
messages: [
{ role: "system", content: systemMsg },
{ role: "user", content: prompt }
]
})
});
const data = await response.json();
return data.choices[0].message.content;
} catch (error) {
console.error('AI API Error:', error);
return "(AI 暂时陷入了沉思,请稍后再试)";
}
const runScene = async (sceneCode, inputs, callbacks = {}) => {
const result = await streamAiScene({
sceneCode,
inputs,
onStart: callbacks.onStart,
onDelta: callbacks.onDelta,
onDone: callbacks.onDone,
onError: callbacks.onError
});
return result.output;
};
/**
* 分析生命事件
* @param {Object} event - 事件对象 { title, time, content }
* @returns {Promise<string>} AI 分析反馈
*/
export const analyzeLifeEvent = async (event) => {
const system = "你是一位温柔的生命引路人,擅长从平凡事件中发掘成长的力量。请分析用户记录的事件,提供情感价值、成长总结和疗愈鼓励。保持字数在150字左右。";
const prompt = `事件标题:${event.title}\n时间:${event.time}\n内容:${event.content}`;
return fetchAI(prompt, system);
export const analyzeLifeEvent = async (event, callbacks = {}) => {
return runScene('life_healing', {
mode: 'life_event_analysis',
prompt: `请分析这段人生事件,并给出温和、具体、可执行的反馈。\n标题:${event.title || ''}\n时间:${event.time || ''}\n内容:${event.content || ''}`,
title: event.title,
time: event.time,
content: event.content
}, callbacks);
};
/**
* 生成爽文剧本
* @param {Object} params - 参数对象 { theme, style, length, character }
* @param {Array} events - 生命事件数组
* @returns {Promise<string>} 生成的剧本内容
*/
export const generateEpicScript = async (params, events = []) => {
const system = `你是一位金牌爽文编剧。根据用户的角色设定和过往经历,生成一段符合用户设定、充满爽感的未来人生剧本。剧本必须包含起承转合,使用【标题】标记段落。`;
const charInfo = `姓名:${params.character.nickname}, 性格:${params.character.mbti}, 兴趣:${params.character.hobbies?.join(',') || ''}, 星座:${params.character.zodiac}`;
const eventSummary = events.map(e => e.title).join(', ');
const prompt = `角色信息:${charInfo}\n过往经历关键词:${eventSummary}\n用户指定主题:${params.theme}\n指定风格:${params.style}\n篇幅要求:${params.length}\n\n请以此创作一段热血、精彩的人生剧本。`;
return fetchAI(prompt, system);
export const generateEpicScript = async (params, events = [], callbacks = {}) => {
return runScene('script_generate', {
prompt: params.theme,
theme: params.theme,
style: params.style,
length: params.length,
events,
character: params.character,
useSocialInsights: params.useSocialInsights
}, callbacks);
};
/**
* 生成实现路径
* @param {string} script - 剧本内容
* @returns {Promise<string>} 生成的路径内容
*/
export const generatePath = async (script) => {
const system = "你是一位人生规划导师。请将用户生成的剧本拆解为现实中可操作的路径。使用【阶段名称】加上具体建议。务必客观、可执行。";
return fetchAI(script, system);
export const generatePath = async (script, callbacks = {}) => {
return runScene('life_healing', {
mode: 'path_generate',
prompt: `请把下面的人生剧本拆解成现实中可执行的路径,按阶段输出。\n\n${script || ''}`,
script
}, callbacks);
};
export default {
+81
View File
@@ -0,0 +1,81 @@
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080';
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 {
return { type: event.type, content: event.data };
}
};
export const streamAiScene = async ({
sceneCode,
inputs = {},
onStart,
onDelta,
onDone,
onError
}) => {
const token = localStorage.getItem('access_token');
const response = await fetch(`${API_BASE_URL}/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) => {
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
};