## 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<SessionInfo>
  speak(sessionId: string, audioBlob: Blob, signal: AbortSignal): AsyncGenerator<SSEEvent>
  getReport(sessionId: string): Promise<Report>
}
```

**实现**: `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` 显示倒计时，不再是纯前端控制
