## Context 当前 `DialogueChatView.vue` 是一个 1041 行的单文件组件,所有对话逻辑(状态管理、录音模拟、评估生成、UI 渲染)全部内联在一个文件中,使用硬编码的 `dialogueScript` 数组驱动对话轮次。 现有架构问题: - 无真实录音能力,录音按钮仅切换 `isRecording` 布尔值 - AI 回复来自本地脚本数组,无 LLM 调用 - 评估数据由 `generateMockEvaluation()` 随机生成 - 无异常处理(网络断开、请求超时、消息重发) - 无消息状态管理(无法区分消息是发送中、流式中还是失败) - UI 和业务逻辑完全耦合,难以测试和维护 技术约束: - 前端:Vue 3 + TypeScript + Composition API,优先使用浏览器原生 API - 后端:独立项目 `cococlass-english-speaking-api`(Python + FastAPI) - LLM 通过 One-Hub 统一网关调用(Chat Completions API) - ASR 和发音评估使用 Azure Speech ## Goals / Non-Goals **Goals:** - 将对话引擎从 UI 组件中解耦为独立的 composable + service 三层架构 - 实现完整的消息状态机(loading / done / error) - 支持 LLM 流式输出(fetch streaming),逐 token 渲染 - 实现异常处理:请求失败 → 显示重试按钮 → 重新发送 - 封装录音采集(MediaRecorder)为独立 composable,含权限处理 - 设计 API 抽象层,支持 mock adapter 和真实 API adapter 无缝切换 - 后端实现对话编排服务,提供 session/speak/report 三个接口 **Non-Goals:** - 不重新设计 UI 外观——保留现有的视觉样式,仅重构逻辑层 - 不涉及离线模式或本地缓存 - 不做自动重试 / 指数退避(v1 先不做,反馈后迭代) - 不做服务端 TTS(v1 用前端 Web Speech API) --- ## 前端架构 ### Decision 1: 三层架构(Service → Composable → Component) **选择**: `llmService`(通信层)→ `useDialogueEngine`(状态编排层)→ `DialogueChatView`(UI 层) **理由**: - Service 层不依赖 Vue,可单元测试,也可在其他场景复用 - Composable 层用 Vue 的 ref/reactive 管理状态 - Component 层变为纯粹的展示层 ### Decision 2: 使用 fetch + ReadableStream **选择**: `fetch()` + `response.body.getReader()` 实现流式读取 **理由**: EventSource 只支持 GET,fetch streaming 支持 POST + AbortController + 自定义 headers ### Decision 3: 消息状态简化为 3 个 **选择**: `loading | done | error`,不细分内部阶段 **理由**: - 用户不关心是 ASR 阶段还是 LLM 阶段,只关心"在处理/好了/出错了" - 错误处理统一:出错 → 显示重试按钮 → 重新调 /speak - 第一版先简单,反馈后再迭代更精细的状态 ### Decision 4: 录音使用浏览器原生 MediaRecorder API **选择**: 直接封装 `MediaRecorder`,不引入第三方录音库 **理由**: 现代浏览器兼容性已足够,避免额外依赖 ### Decision 5: API Adapter 对接后端 3 个接口 **选择**: `DialogueAPI` 接口对应后端的 session / speak / report ```typescript interface DialogueAPI { createSession(config: SessionConfig): Promise speak(sessionId: string, audioBlob: Blob, signal: AbortSignal): AsyncGenerator getReport(sessionId: string): Promise } ``` **实现**: `MockDialogueAPI`(开发用)和 `RealDialogueAPI`(对接真实后端) 通过前端 prop `mode: 'preview' | 'real'` 切换 ### Decision 6: TTS 使用前端 Azure TTS **选择**: AI 消息 `done` 后,前端整段文本调用 Azure TTS 合成语音并播放 **理由**: - 前端已通过 SSE 接收完整文本,直接调 Azure TTS 最简单 - AI 回复限定 1-2 句(system prompt 约束),不需要分句 TTS - 不做服务端 TTS,后端不涉及语音合成 --- ## 后端架构 ### 项目:cococlass-english-speaking-api 独立编排服务,串联 Azure Speech 和 One-Hub,管理对话 session 状态。 ``` 前端 │ ▼ cococlass-english-speaking-api(编排层) ├──→ Azure Speech ASR + 发音评估 ├──→ One-Hub LLM 流式对话(Chat Completions) ├──→ MySQL session + 消息 + 评估结果 └──→ S3 音频文件存储 ``` ### 技术栈 | 层 | 选择 | |----|------| | 框架 | FastAPI + uvicorn | | LLM | Chat Completions via One-Hub(openai SDK) | | ASR | Azure Speech(纯转录模式) | | 发音评估 | Azure Speech Pronunciation Assessment(后台) | | SSE | FastAPI 原生 EventSourceResponse + ServerSentEvent | | 数据库 | MySQL + asyncmy + SQLAlchemy 2.0 | | 存储 | S3(boto3) | | 配置 | pydantic-settings + python-dotenv | ### Decision 7: 不使用 Assistants API **理由**: 口语对话是简单一问一答,不需要 Thread/Run 机制。对话历史要存自己 DB,Assistants 的 Thread 管理是重复功能。 ### Decision 8: 后端统一编排,前端只发一个请求 **理由**: 前端不应知道后端内部有几步(ASR/LLM/评估)。音频只上传一次,错误处理统一。 ### Decision 9: 对话中不展示评分 **理由**: 保证练习流畅度。发音评估后台静默执行,结果页才展示。 ### Decision 10: Protocol + 依赖注入 ASRProvider / LLMProvider / PronunciationAssessor / AudioStorage 四个 Protocol 接口,具体实现可替换可测试。 ### Decision 11: 不用 Redis **理由**: 3 轮对话只有 3 次 DB 查询,不值得加缓存层。 ### API 设计 ``` POST /api/speaking/dialogue/session → JSON { sessionId, aiMessage } POST /api/speaking/dialogue/speak → SSE { transcript, token, done } GET /api/speaking/dialogue/report → JSON { 对话历史 + 评分 + LLM 总结 } ``` ### 数据模型 - **DialogueSession**: id, user_id, topic, role_config, total_rounds, current_round, status, system_prompt, expires_at, summary, created_at, completed_at - **DialogueMessage**: id, session_id, round, role, content, audio_url, created_at - **PronunciationEvaluation**: id, message_id, session_id, round, status, accuracy/fluency/completeness/prosody scores, word_analysis, created_at, completed_at --- ## Risks / Trade-offs - **[MediaRecorder 格式兼容性]** → Safari 不支持 webm,需 fallback 到 mp4/wav - **[流式解析差异]** → 在 adapter 层做格式解析,engine 层只关心 token string - **[录音权限拒绝]** → composable 暴露 permissionState,UI 显示引导提示 - **[内存泄漏]** → 在 composable 的 `onUnmounted` 中统一清理录音 Blob - **[后台评估未完成]** → 前端轮询 GET /report(每 2 秒),30 秒超时后展示已有结果,缺失的标记为"评估不可用"。评估记录在 speak 主流程中创建(保证记录一定存在),后台任务只负责更新结果 - **[定时结束]** → 后端通过 `expires_at` 判断,`/speak` 时检查是否超时。前端用 `expires_at` 显示倒计时,不再是纯前端控制