jimmylee 3 месяцев назад
Родитель
Сommit
2d141842e0

+ 2 - 0
openspec/changes/dialogue-chat-engine/.openspec.yaml

@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-04-08

+ 165 - 0
openspec/changes/dialogue-chat-engine/design.md

@@ -0,0 +1,165 @@
+## 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 使用前端 Web Speech API(v1)
+
+**选择**: AI 消息 `done` 后调用 `speechSynthesis.speak()` 朗读
+
+**理由**: 零后端改动,3 行代码立即可用。质量不够后续升级 Azure 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 StreamingResponse |
+| 数据库 | 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    → 延后(结果页方案待确认)
+```
+
+### 数据模型
+
+- **DialogueSession**: id, user_id, topic, role_config, total_rounds, current_round, status, system_prompt, 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
+- **[后台评估未完成]** → /report 接口最多等 5 秒,超时返回部分结果

+ 45 - 0
openspec/changes/dialogue-chat-engine/proposal.md

@@ -0,0 +1,45 @@
+## Why
+
+当前 `DialogueChatView.vue` 的对话逻辑全部基于硬编码的 mock 脚本,没有真实的录音采集、LLM 对话、发音评估能力。要实现完整的英语口语对话功能,需要:
+- 前端:将对话引擎从 UI 层解耦,建立三层架构,处理录音 → 上传 → 流式回复的完整 pipeline
+- 后端:新建独立编排服务 `cococlass-english-speaking-api`,串联 ASR、LLM、发音评估
+
+## What Changes
+
+### 前端
+- **新增 `llmService.ts`**:API 通信层,封装 fetch + ReadableStream 实现流式 SSE 读取
+- **新增 `useDialogueEngine.ts`**:Vue composable,管理对话状态机(loading / done / error)
+- **新增 `useAudioRecorder.ts`**:Vue composable,封装 MediaRecorder API 实现录音采集
+- **重构 `DialogueChatView.vue`**:移除所有 mock 逻辑,改为纯 UI 层
+- **扩展 `PreviewChatMessage` 类型**:新增 `status`(loading/done/error)、`error`、`audioBlob` 字段
+- **新增 TTS**:AI 消息完成后调用 Web Speech API 朗读(v1)
+- **新增倒计时**:支持限制对话时间,时间到后让当前轮完成再自动结束(纯前端,不影响后端)
+
+### 后端(cococlass-english-speaking-api)
+- **POST /session**:创建对话 + AI 开场白
+- **POST /speak**:接收音频 → ASR 转录 → LLM 流式回复(SSE)→ 后台静默发音评估
+- **GET /report**:返回完整对话历史 + 评估结果
+- **init.sql**:3 张表建表脚本(DialogueSession / DialogueMessage / PronunciationEvaluation),git 管理,手动执行
+
+## Capabilities
+
+### New Capabilities
+- `dialogue-engine`: 对话引擎核心——消息状态机、轮次控制、重试、流式输出
+- `audio-recording`: 录音采集——MediaRecorder 封装、权限处理
+- `llm-streaming`: LLM 流式通信——fetch + ReadableStream SSE 解析、超时控制、错误处理
+
+### Modified Capabilities
+_(无已有 specs 需要修改)_
+
+## Impact
+
+- **前端受影响文件**:
+  - `src/views/Editor/EnglishSpeaking/preview/DialogueChatView.vue`(重构)
+  - `src/types/englishSpeaking.ts`(扩展类型)
+  - `src/views/Editor/EnglishSpeaking/preview/TopicDiscussionPreview.vue`(适配)
+- **前端新增文件**:
+  - `src/views/Editor/EnglishSpeaking/composables/useDialogueEngine.ts`
+  - `src/views/Editor/EnglishSpeaking/composables/useAudioRecorder.ts`
+  - `src/views/Editor/EnglishSpeaking/services/llmService.ts`
+- **前端依赖**:无新增 npm 依赖,使用浏览器原生 API
+- **后端项目**:`cococlass-english-speaking-api`(独立 repo)

+ 67 - 0
openspec/changes/dialogue-chat-engine/specs/audio-recording/spec.md

@@ -0,0 +1,67 @@
+## ADDED Requirements
+
+### Requirement: Audio recording lifecycle
+录音 composable SHALL 管理完整的录音生命周期:请求权限 → 开始录音 → 录音中 → 停止录音 → 返回音频 Blob。
+
+#### Scenario: Successful recording
+- **WHEN** 用户点击录音按钮且浏览器麦克风权限已授予
+- **THEN** MediaRecorder 开始采集音频,`isRecording` 状态变为 `true`,录音计时器开始计时
+
+#### Scenario: Stop recording
+- **WHEN** 用户点击停止录音按钮
+- **THEN** MediaRecorder 停止采集,收集到的 audio chunks 合并为 Blob,通过回调或返回值提供给调用方
+
+### Requirement: Microphone permission handling
+系统 SHALL 在首次录音时请求麦克风权限,并优雅处理权限拒绝场景。
+
+#### Scenario: Permission granted
+- **WHEN** 用户首次点击录音按钮且浏览器弹出权限请求
+- **THEN** 用户授权后开始录音,`permissionState` 变为 `granted`
+
+#### Scenario: Permission denied
+- **WHEN** 用户拒绝麦克风权限
+- **THEN** `permissionState` 变为 `denied`,UI 显示引导提示(如何在浏览器设置中开启权限),录音按钮保持可点击但点击后显示提示
+
+#### Scenario: Permission previously granted
+- **WHEN** 用户之前已授权且再次进入页面
+- **THEN** 直接开始录音,无权限弹窗
+
+### Requirement: Audio format compatibility
+录音 composable SHALL 自动选择浏览器支持的最佳音频格式。
+
+#### Scenario: Browser supports webm
+- **WHEN** `MediaRecorder.isTypeSupported('audio/webm;codecs=opus')` 返回 true
+- **THEN** 使用 `audio/webm` 格式录制
+
+#### Scenario: Browser does not support webm (Safari)
+- **WHEN** webm 不被支持
+- **THEN** fallback 到 `audio/mp4` 或 `audio/wav`
+
+### Requirement: Silence detection
+录音过程中系统 SHALL 检测长时间沉默并给出提示。
+
+#### Scenario: Silence detected
+- **WHEN** 录音开始后连续 5 秒未检测到有效语音输入
+- **THEN** 显示沉默提示浮层,包含鼓励性的句子提示
+
+#### Scenario: User resumes speaking
+- **WHEN** 沉默提示显示后用户开始说话
+- **THEN** 沉默提示自动消失
+
+### Requirement: Recording timer
+录音过程中 SHALL 显示实时录音时长。
+
+#### Scenario: Timer counting
+- **WHEN** 录音正在进行
+- **THEN** 状态栏显示已录音秒数,每秒更新一次
+
+#### Scenario: Timer reset
+- **WHEN** 录音停止
+- **THEN** 录音计时器归零
+
+### Requirement: Resource cleanup
+录音相关的资源 SHALL 在组件卸载时完全释放。
+
+#### Scenario: Component unmount during recording
+- **WHEN** 用户在录音过程中离开页面(组件 unmount)
+- **THEN** 系统自动停止 MediaRecorder、释放 MediaStream tracks、清除定时器

+ 74 - 0
openspec/changes/dialogue-chat-engine/specs/dialogue-engine/spec.md

@@ -0,0 +1,74 @@
+## ADDED Requirements
+
+### Requirement: Message status lifecycle
+每条对话消息 SHALL 拥有独立的状态字段,使用 3 个状态:
+- `loading`: 处理中(发送/转录/流式输出)
+- `done`: 完成
+- `error`: 失败
+
+#### Scenario: Student message sent successfully
+- **WHEN** 学生完成录音并发送消息
+- **THEN** 消息状态从 `loading` 变为 `done`,消息内容来自 ASR 转写结果
+
+#### Scenario: AI reply streams in
+- **WHEN** 学生消息发送成功后触发 AI 回复
+- **THEN** AI 消息进入 `loading` 状态(显示 typing 动画),收到 token 后逐字渲染,流结束后变为 `done`
+
+#### Scenario: Request fails at any stage
+- **WHEN** 网络异常或后端错误导致请求失败(不区分 ASR/LLM 阶段)
+- **THEN** 出错的消息状态变为 `error`,显示「重试」按钮
+
+### Requirement: Message retry
+系统 SHALL 支持对 `error` 状态的消息进行重试操作。重试按钮 SHALL 显示在出错消息的下方。
+
+#### Scenario: Retry a failed student message
+- **WHEN** 用户点击学生消息下方的「重试」按钮
+- **THEN** 系统用缓存的 audioBlob 重新调用 `/speak`,整个流程重走
+
+#### Scenario: Retry a failed AI message (stream interrupted)
+- **WHEN** AI 消息流式中断,用户点击 AI 消息下方的「重新生成」按钮
+- **THEN** 系统清空 AI 消息内容,重新调用 `/speak`(后端幂等,不重复存学生消息),重新流式填充
+
+#### Scenario: User chooses to re-record instead of retry
+- **WHEN** 学生消息发送失败后,用户直接点击录音按钮录新音频
+- **THEN** 系统丢弃失败消息,用新录音发起新的 `/speak` 请求
+
+### Requirement: AudioBlob lifecycle
+录音 Blob SHALL 在该轮对话成功完成前保持缓存,以支持重试。
+
+#### Scenario: Retain blob until round completes
+- **WHEN** 学生完成录音
+- **THEN** audioBlob 保持在内存中,直到该轮 AI 回复 `done` 后才可释放
+
+### Requirement: Round progression
+对话引擎 SHALL 按轮次推进对话。每轮结束由后端 SSE `done` 事件触发。
+
+#### Scenario: Normal round progression
+- **WHEN** 收到 SSE `done` 事件且 `isComplete` 为 false
+- **THEN** 录音按钮恢复可用,等待学生下一轮录音;下一轮的 AI 消息由后端在 `done` 事件中返回
+
+#### Scenario: Dialogue complete
+- **WHEN** 收到 SSE `done` 事件且 `isComplete` 为 true
+- **THEN** 触发 `complete` 事件,前端转到报告页
+
+#### Scenario: First round initialization
+- **WHEN** 调用 `POST /session` 创建对话
+- **THEN** 后端返回 AI 开场白,前端显示第一条 AI 消息,学生进入可录音状态
+
+### Requirement: Input locking
+系统 SHALL 在消息处理过程中禁用录音按钮,保证一次只能有一条处理中的消息。
+
+#### Scenario: Lock during processing
+- **WHEN** 有任何消息处于 `loading` 状态
+- **THEN** 录音按钮处于禁用状态
+
+#### Scenario: Unlock after completion or error
+- **WHEN** 当前轮所有消息都是 `done` 或 `error` 状态
+- **THEN** 录音按钮恢复可用
+
+### Requirement: Auto scroll
+消息列表 SHALL 在新内容到达时自动滚动到底部。
+
+#### Scenario: Auto scroll on new content
+- **WHEN** 新消息添加或流式输出产生新内容
+- **THEN** 自动滚动到底部

+ 52 - 0
openspec/changes/dialogue-chat-engine/specs/llm-streaming/spec.md

@@ -0,0 +1,52 @@
+## ADDED Requirements
+
+### Requirement: Streaming chat response
+服务层 SHALL 使用 `fetch` + `ReadableStream` 实现流式对话,通过 `AsyncGenerator<SSEEvent>` 供上层消费。
+
+#### Scenario: Successful streaming
+- **WHEN** 调用 `speak(sessionId, audioBlob)` 发起请求
+- **THEN** 依次 yield `transcript`、`token`(多次)、`done` 事件
+
+#### Scenario: Empty response
+- **WHEN** 后端返回空的 stream(0 tokens)
+- **THEN** generator 正常结束,不抛异常
+
+### Requirement: Request cancellation
+每个流式请求 SHALL 支持通过 `AbortController` 取消。
+
+#### Scenario: User cancels during streaming
+- **WHEN** 流式输出进行中调用 `signal.abort()`
+- **THEN** fetch 请求被取消,generator 抛出 `AbortError`,上层保留已接收内容
+
+### Requirement: Error handling
+请求失败时 SHALL 统一抛出错误,不区分错误类型(v1 简化版)。
+
+#### Scenario: Request fails
+- **WHEN** 网络断开、超时、或后端返回错误
+- **THEN** 抛出 Error,上层将消息状态设为 `error`,显示重试按钮
+
+### Requirement: Adapter interface
+服务层 SHALL 通过 `DialogueAPI` 接口抽象后端通信,支持 mock 和真实 API 切换。
+
+```typescript
+interface DialogueAPI {
+  createSession(config: SessionConfig): Promise<SessionInfo>
+  speak(sessionId: string, audioBlob: Blob, signal: AbortSignal): AsyncGenerator<SSEEvent>
+  getReport(sessionId: string): Promise<Report>
+}
+```
+
+#### Scenario: Using mock adapter
+- **WHEN** `mode` 为 `preview` 时
+- **THEN** 使用 `MockDialogueAPI`,模拟 transcript + 逐 token 输出
+
+#### Scenario: Using real adapter
+- **WHEN** `mode` 为 `real` 时
+- **THEN** 使用 `RealDialogueAPI`,对接后端 3 个接口
+
+### Requirement: SSE response parsing
+服务层 SHALL 解析后端返回的 SSE 格式流。
+
+#### Scenario: Parse SSE events
+- **WHEN** 后端返回 `event: transcript\ndata: {...}\n\n` 格式
+- **THEN** 根据 event type 解析为对应的 SSEEvent 对象(transcript / token / done)

+ 86 - 0
openspec/changes/dialogue-chat-engine/tasks.md

@@ -0,0 +1,86 @@
+## 前端
+
+### 1. 类型定义与基础设施
+
+- [ ] 1.1 扩展 `PreviewChatMessage` 类型:新增 `status`(loading/done/error)、`error`、`audioBlob` 字段
+- [ ] 1.2 定义 `DialogueAPI` 接口:`createSession` / `speak` / `getReport` 方法
+- [ ] 1.3 定义 SSE 事件类型:`TranscriptEvent` / `TokenEvent` / `DoneEvent`
+- [ ] 1.4 创建 composables 和 services 目录结构
+
+### 2. API 通信层(llmService.ts)
+
+- [ ] 2.1 实现 SSE 响应解析器:根据 event type 解析 transcript / token / done
+- [ ] 2.2 实现 `RealDialogueAPI`:fetch + ReadableStream,对接后端 3 个接口
+- [ ] 2.3 实现 `MockDialogueAPI`:模拟 transcript + 逐 token 输出
+- [ ] 2.4 实现 AbortController 支持
+
+### 3. 录音采集(useAudioRecorder.ts)
+
+- [ ] 3.1 实现麦克风权限请求与状态管理
+- [ ] 3.2 实现 MediaRecorder 封装:start / stop,自动检测 mimeType
+- [ ] 3.3 实现录音计时器
+- [ ] 3.4 实现资源清理:onUnmounted 中释放 MediaStream
+
+### 4. 对话引擎(useDialogueEngine.ts)
+
+- [ ] 4.1 实现消息列表管理:`messages` ref
+- [ ] 4.2 实现 `sendStudentMessage(audioBlob)`:添加学生消息 → 调 speak → 流式消费 → 完成/失败
+- [ ] 4.3 实现重试:学生消息失败 → 用缓存 audioBlob 重新调 speak
+- [ ] 4.4 实现重新生成:AI 消息中断 → 清空内容重新调 speak
+- [ ] 4.5 实现轮次推进:收到 done 事件后判断是否结束
+- [ ] 4.6 实现录音按钮锁定:loading 时禁用
+- [ ] 4.7 实现 adapter 注入:通过 mode prop 选择 Mock/Real
+- [ ] 4.8 实现 TTS:AI 消息 done 后调用 Web Speech API 朗读
+
+### 5. 重构 DialogueChatView.vue
+
+- [ ] 5.1 移除所有 mock 逻辑,改为从 useDialogueEngine 获取状态
+- [ ] 5.2 接入 useAudioRecorder:录音按钮绑定 start/stop
+- [ ] 5.3 新增错误状态 UI:error 消息显示「重试」/「重新生成」按钮
+- [ ] 5.4 新增流式输出 UI:loading 消息显示 typing 动画
+- [ ] 5.5 实现自动滚动
+- [ ] 5.6 接入麦克风权限引导 UI
+
+### 6. 前端验证
+
+- [ ] 6.1 编译验证:无 TypeScript 错误
+- [ ] 6.2 Mock 模式端到端:录音 → 发送 → 流式回复 → 轮次推进
+- [ ] 6.3 异常场景验证:模拟请求失败、重试
+
+---
+
+## 后端(cococlass-english-speaking-api)
+
+### 7. 项目搭建
+
+- [ ] 7.1 初始化项目:uv init + 安装依赖
+- [ ] 7.2 创建目录结构
+- [ ] 7.3 配置 pydantic-settings + .env
+- [ ] 7.4 配置 SQLAlchemy 2.0 async engine + 数据模型
+
+### 8. Provider 实现
+
+- [ ] 8.1 定义 Protocol 接口(ASR / LLM / PronunciationAssessor / AudioStorage)
+- [ ] 8.2 实现 Azure Speech ASR Provider
+- [ ] 8.3 实现 Azure Speech Pronunciation Assessment Provider
+- [ ] 8.4 实现 One-Hub LLM Provider(Chat Completions 流式)
+- [ ] 8.5 实现 S3 Audio Storage Provider
+
+### 9. 核心服务
+
+- [ ] 9.1 实现 DialogueService.create_session
+- [ ] 9.2 实现 DialogueService.speak(ASR → SSE transcript → LLM 流式 → SSE token/done)
+- [ ] 9.3 实现后台发音评估(asyncio.create_task)
+- [ ] 9.4 实现 DialogueService.get_report(延后,结果页方案待确认)
+
+### 10. API 路由
+
+- [ ] 10.1 实现 POST /api/speaking/dialogue/session
+- [ ] 10.2 实现 POST /api/speaking/dialogue/speak(StreamingResponse)
+- [ ] 10.3 实现 GET /api/speaking/dialogue/report(延后)
+
+### 11. 后端验证
+
+- [ ] 11.1 启动验证:uvicorn 启动无报错
+- [ ] 11.2 接口验证:用 httpx/curl 测试 3 个接口
+- [ ] 11.3 前后端联调:前端 RealDialogueAPI 对接后端