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
+8 -2
View File
@@ -16,6 +16,7 @@ const PathView = ({ onGoToScript }) => {
const { getSelectedScript, selectedPath, setPath, loadPath, deletePath, selectedScriptId } = useStore();
const [isLoading, setIsLoading] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [streamPath, setStreamPath] = useState('');
const selectedScript = getSelectedScript();
@@ -37,8 +38,12 @@ const PathView = ({ onGoToScript }) => {
setIsLoading(true);
try {
const path = await generatePath(selectedScript.content);
setStreamPath('');
const path = await generatePath(selectedScript.content, {
onDelta: (_delta, output) => setStreamPath(output)
});
await setPath(path, selectedScriptId);
setStreamPath('');
} catch (error) {
console.error('Failed to generate path:', error);
} finally {
@@ -77,7 +82,8 @@ const PathView = ({ onGoToScript }) => {
});
};
const pathSteps = parsePathSteps(selectedPath);
const visiblePath = streamPath || selectedPath;
const pathSteps = parsePathSteps(visiblePath);
// 无剧本时显示提示
if (!selectedScript) {
+35 -3
View File
@@ -4,6 +4,7 @@ import { GlassCard, GlassButton, GlassInput, GlassSelect } from '../components/u
import Modal from '../components/Modal';
import useStore from '../store/useStore';
import { scriptStyles, scriptLengths } from '../utils/constants';
import { generateEpicScript } from '../services/ai';
/**
* ScriptView 组件
@@ -37,6 +38,7 @@ const ScriptView = ({ onOpenProfile }) => {
const [style, setStyle] = useState(scriptStyles[0].value);
const [length, setLength] = useState(scriptLengths[0].value);
const [isLoading, setIsLoading] = useState(false);
const [streamContent, setStreamContent] = useState('');
// 编辑模态框状态
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
@@ -62,16 +64,23 @@ const ScriptView = ({ onOpenProfile }) => {
setIsLoading(true);
try {
// 直接调用后端创建接口,由后端调用AI生成
setStreamContent('');
const content = await generateEpicScript(
{ theme, style, length, character: registrationData },
lifeEvents,
{ onDelta: (_delta, output) => setStreamContent(output) }
);
await addScript({
theme,
style,
length,
content,
character: registrationData,
events: lifeEvents
});
setTheme('');
setStreamContent('');
} catch (error) {
console.error('Failed to generate script:', error);
} finally {
@@ -114,16 +123,24 @@ const ScriptView = ({ onOpenProfile }) => {
setIsLoading(true);
try {
setStreamContent('');
const content = await generateEpicScript(
{ theme: editForm.theme, style: editForm.style, length: editForm.length, character: registrationData },
lifeEvents,
{ onDelta: (_delta, output) => setStreamContent(output) }
);
await updateScript({
id: editingScript.id,
theme: editForm.theme,
style: editForm.style,
length: editForm.length,
content,
character: registrationData,
events: lifeEvents,
regenerateContent: true // 标记需要重新生成AI内容
regenerateContent: false
});
closeEditModal();
setStreamContent('');
} catch (error) {
console.error('Failed to update script:', error);
} finally {
@@ -289,7 +306,22 @@ const ScriptView = ({ onOpenProfile }) => {
{/* 右侧剧本展示区 */}
<div className="lg:col-span-8">
<div className="h-full">
{selectedScript ? (
{isLoading && streamContent ? (
<GlassCard className="h-full overflow-y-auto custom-scrollbar border-orange-200/20 shadow-2xl animate-fade-in" padding="lg">
<div className="prose prose-invert max-w-none">
<div className="flex justify-between items-center mb-8 pb-4 border-b border-white/5">
<div>
<h4 className="text-2xl font-serif text-orange-200">{theme}</h4>
<p className="text-[10px] text-white/30 mt-1 uppercase tracking-widest">正在生成</p>
</div>
<BookOpen className="text-white/20" />
</div>
<div className="text-white/70 leading-loose whitespace-pre-wrap space-y-6 text-sm">
{streamContent}
</div>
</div>
</GlassCard>
) : selectedScript ? (
<GlassCard className="h-full overflow-y-auto custom-scrollbar border-orange-200/20 shadow-2xl animate-fade-in relative group" padding="lg">
{/* 编辑按钮 */}
<button
+23 -2
View File
@@ -3,6 +3,7 @@ import { Plus, Wind, Sparkles, Pencil, Trash2 } from 'lucide-react';
import { GlassCard, GlassButton, GlassInput, GlassTextarea } from '../components/ui';
import Modal from '../components/Modal';
import useStore from '../store/useStore';
import { analyzeLifeEvent } from '../services/ai';
/**
* 格式化 AI 反馈内容的组件
@@ -91,6 +92,7 @@ const TimelineView = () => {
// 模态框状态
const [isModalOpen, setIsModalOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [streamFeedback, setStreamFeedback] = useState('');
// 编辑模式状态:null 表示新增模式,有值表示编辑模式(存储事件 ID)
const [editingEventId, setEditingEventId] = useState(null);
@@ -111,6 +113,7 @@ const TimelineView = () => {
const openAddModal = () => {
setEditingEventId(null);
setEventForm({ title: '', time: '', content: '' });
setStreamFeedback('');
setIsModalOpen(true);
};
@@ -125,6 +128,7 @@ const TimelineView = () => {
time: event.time || '',
content: event.content || ''
});
setStreamFeedback(event.aiFeedback || '');
setIsModalOpen(true);
};
@@ -135,6 +139,7 @@ const TimelineView = () => {
setIsModalOpen(false);
setEditingEventId(null);
setEventForm({ title: '', time: '', content: '' });
setStreamFeedback('');
};
/**
@@ -149,16 +154,23 @@ const TimelineView = () => {
setIsLoading(true);
try {
setStreamFeedback('');
const aiFeedback = await analyzeLifeEvent(eventForm, {
onDelta: (_delta, output) => setStreamFeedback(output)
});
if (editingEventId) {
// 编辑模式:调用更新接口
await updateLifeEvent({
id: editingEventId,
...eventForm
...eventForm,
aiFeedback
});
} else {
// 新增模式:调用添加接口
await addLifeEvent({
...eventForm
...eventForm,
aiFeedback
});
}
@@ -311,6 +323,15 @@ const TimelineView = () => {
onChange={(v) => setEventForm(prev => ({ ...prev, content: v }))}
rows={5}
/>
{streamFeedback && (
<div className="ai-glow-card p-4 rounded-2xl bg-orange-200/[0.02] border border-orange-200/5">
<div className="flex items-center gap-2 mb-3">
<Sparkles className="w-3 h-3 text-orange-200" />
<span className="text-[9px] uppercase tracking-[0.2em] text-orange-200/60 font-bold">实时疗愈反馈</span>
</div>
<p className="text-xs italic text-white/50 leading-loose whitespace-pre-wrap">{streamFeedback}</p>
</div>
)}
<GlassButton
variant="primary"
onClick={handleSubmit}