diff --git a/.github/workflows/app-expo-deploy.yml b/.github/workflows/app-expo-deploy.yml index d061a09..b27b1a6 100644 --- a/.github/workflows/app-expo-deploy.yml +++ b/.github/workflows/app-expo-deploy.yml @@ -1,10 +1,14 @@ -# App Expo 统一部署 Pipeline +# App Expo:CI 内生成 Android 签名 APK(expo prebuild + Gradle assembleRelease) +# 使用 app-expo/plugins/withAndroidReleaseSigning:在 android/app 放置 keystore.properties + jks 后打 release 包。 # # 环境映射(按触发源自动推断): # main → dev (开发 + 内部测试) -# v*.*.* → prod (正式发布) +# v*.*.* → prod (正式发布 + GitHub Release 附带 APK) # # 手动触发:workflow_dispatch 可选择 dev / stage / prod +# +# Repository secrets(与 android-release.yml 共用同一套即可): +# ANDROID_KEYSTORE_BASE64 / ANDROID_STORE_PASSWORD / ANDROID_KEY_ALIAS / ANDROID_KEY_PASSWORD name: App Expo Deploy @@ -27,7 +31,7 @@ on: - prod default: dev version: - description: '版本号 (prod 时使用,如 1.0.0)' + description: '版本号 (prod 手动发版时使用,如 1.0.0)' required: false type: string @@ -40,10 +44,10 @@ env: jobs: deploy: - name: "Build & Deploy" + name: "Android APK" runs-on: ubuntu-latest permissions: - contents: read + contents: write steps: - name: Checkout code @@ -90,13 +94,9 @@ jobs: *) node scripts/use-env.js development ;; esac - - name: Export web build - working-directory: app-expo - run: npx expo export -p web - - - name: Determine version (prod) - if: steps.env.outputs.env == 'prod' + - name: Determine version id: version + working-directory: app-expo run: | if [[ "${{ github.ref }}" == refs/tags/v* ]]; then VERSION="${GITHUB_REF#refs/tags/v}" @@ -105,17 +105,72 @@ jobs: VERSION="${{ github.event.inputs.version }}" TAG_NAME="v${VERSION}" else - VERSION=$(node -p "require('./app-expo/package.json').version") + VERSION=$(node -p "require('./package.json').version") TAG_NAME="v${VERSION}" fi + VERSION_CODE="${GITHUB_RUN_NUMBER}" echo "version=${VERSION}" >> $GITHUB_OUTPUT echo "tag_name=${TAG_NAME}" >> $GITHUB_OUTPUT + echo "version_code=${VERSION_CODE}" >> $GITHUB_OUTPUT + echo "apk_name=${{ env.APP_NAME }}-v${VERSION}-release.apk" >> $GITHUB_OUTPUT + echo "版本名: ${VERSION}, versionCode: ${VERSION_CODE}, tag: ${TAG_NAME}" - - name: Create release zip (prod) - if: steps.env.outputs.env == 'prod' + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Expo prebuild (Android) + working-directory: app-expo + run: npx expo prebuild --platform android --clean + + - name: Decode keystore + working-directory: app-expo/android/app run: | - cd app-expo/dist - zip -r ../${{ env.APP_NAME }}-v${{ steps.version.outputs.version }}-web.zip . + echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode > release-keystore.jks + cat > keystore.properties <> $GITHUB_OUTPUT + du -h "$FINAL_PATH" + + - name: Upload APK artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.version.outputs.apk_name }} + path: app-expo/android/${{ steps.apk.outputs.apk_path }} + retention-days: ${{ steps.env.outputs.env == 'prod' && '90' || (steps.env.outputs.env == 'dev' && '14' || '30') }} - name: Generate Release Notes (prod) if: steps.env.outputs.env == 'prod' && startsWith(github.ref, 'refs/tags/') @@ -130,7 +185,7 @@ jobs: fi { echo "notes<> $GITHUB_OUTPUT - - name: Upload artifact (dev / stage) - if: steps.env.outputs.env != 'prod' - uses: actions/upload-artifact@v4 - with: - name: app-expo-web-${{ steps.env.outputs.env }} - path: app-expo/dist - retention-days: ${{ steps.env.outputs.env == 'dev' && '14' || '30' }} - - - name: Upload artifact (prod) - if: steps.env.outputs.env == 'prod' - uses: actions/upload-artifact@v4 - with: - name: ${{ env.APP_NAME }}-v${{ steps.version.outputs.version }}-web - path: app-expo/${{ env.APP_NAME }}-v${{ steps.version.outputs.version }}-web.zip - retention-days: 90 - - name: Create GitHub Release (prod) if: steps.env.outputs.env == 'prod' uses: softprops/action-gh-release@v2 @@ -166,8 +205,6 @@ jobs: body: ${{ steps.release_notes.outputs.notes || 'Release' }} draft: false prerelease: false - files: app-expo/${{ env.APP_NAME }}-v${{ steps.version.outputs.version }}-web.zip + files: app-expo/android/${{ steps.apk.outputs.apk_path }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - permissions: - contents: write diff --git a/api/docs/README.md b/api/docs/README.md deleted file mode 100644 index 33bb637..0000000 --- a/api/docs/README.md +++ /dev/null @@ -1,84 +0,0 @@ -# API 文档索引 - -> Life Echo API 详细文档目录 - -## 📚 文档导航 - -### 🚀 快速开始 - -- **[本地开发环境配置](./本地开发环境配置.md)** - 详细的开发环境搭建指南 - - 架构概述 - - 前置要求 - - 快速启动步骤 - - 服务说明(FastAPI、Redis、Celery) - - 生产环境部署 - - 常见问题 - -### 🔌 WebSocket 文档 - -- **[WebSocket 快速测试指南](./WebSocket快速测试指南.md)** - 快速上手 WebSocket 测试 - - 连接地址 - - 消息类型速查 - - 测试场景 - - 常见问题 - -- **[WebSocket 测试文档](./WebSocket测试文档.md)** - WebSocket 接口详细文档 - - 接口信息 - - 消息类型定义 - - 消息格式规范 - - 完整测试示例 - - 错误处理 - -### 💬 功能说明 - -- **[文字交流模式说明](./文字交流模式说明.md)** - 文字对话模式详细说明 - - 功能特性 - - WebSocket 消息格式 - - 使用流程 - - 已移除/保留的功能 - - 注意事项 - -### 🧪 测试文档 - -- **[测试脚本使用说明](./测试脚本使用说明.md)** - 自动化测试脚本使用指南 - - 前置条件 - - 运行测试 - - 测试流程 - - 自定义测试内容 - - 常见问题 - -## 📖 文档分类 - -### 按用途分类 - -| 文档 | 用途 | 适合人群 | -|------|------|----------| -| 本地开发环境配置 | 环境搭建 | 新开发者 | -| WebSocket 快速测试指南 | 快速测试 | 前端开发者、测试人员 | -| WebSocket 测试文档 | 详细接口文档 | 后端开发者、集成开发者 | -| 文字交流模式说明 | 功能说明 | 所有开发者 | -| 测试脚本使用说明 | 自动化测试 | 测试人员、CI/CD | - -### 按阶段分类 - -**开发阶段**: -1. [本地开发环境配置](./本地开发环境配置.md) - 搭建开发环境 -2. [文字交流模式说明](./文字交流模式说明.md) - 了解功能特性 - -**测试阶段**: -1. [WebSocket 快速测试指南](./WebSocket快速测试指南.md) - 快速测试 -2. [WebSocket 测试文档](./WebSocket测试文档.md) - 详细测试 -3. [测试脚本使用说明](./测试脚本使用说明.md) - 自动化测试 - -## 🔗 相关链接 - -- [API README](../README.md) - API 服务主文档 -- [项目根目录 README](../../README.md) - 项目总览 - -## 📝 文档更新 - -文档会随着项目发展持续更新。如有问题或建议,请提交 Issue 或 Pull Request。 - ---- - -**Life Echo API 文档** - 让开发更简单 📚✨ diff --git a/api/docs/WebSocket快速测试指南.md b/api/docs/WebSocket快速测试指南.md deleted file mode 100644 index 8735779..0000000 --- a/api/docs/WebSocket快速测试指南.md +++ /dev/null @@ -1,103 +0,0 @@ -# Life Echo WebSocket 快速测试指南 - -## 🚀 快速开始 - -### 1. 连接地址 -``` -ws://localhost:8000/ws/conversation/{conversation_id} -``` - -### 2. 生成对话ID -```bash -# Python -python -c "import uuid; print(uuid.uuid4())" - -# 或使用在线工具:https://www.uuidgenerator.net/ -``` - -### 3. 在 Apifox 中操作 - -1. **新建 WebSocket 请求** - - 协议:WebSocket - - URL:`ws://localhost:8000/ws/conversation/你的对话ID` - -2. **点击连接** - - 应该立即收到 `connect` 消息 - -3. **发送音频块** - ```json - { - "type": "audio_chunk", - "conversation_id": "你的对话ID", - "data": { - "audio_base64": "UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQAAAAA=" - } - } - ``` - -4. **接收响应**(按顺序) - - `transcript` - 语音转文字 - - `agent_response` - Agent 回应 - - `tts_audio` - TTS 音频 - -5. **结束对话** - ```json - { - "type": "end_conversation", - "conversation_id": "你的对话ID" - } - ``` - ---- - -## 📋 消息类型速查 - -### 客户端发送 - -| 类型 | 说明 | JSON 示例 | -|------|------|-----------| -| `audio_chunk` | 发送音频 | `{"type":"audio_chunk","data":{"audio_base64":"..."}}` | -| `end_conversation` | 结束对话 | `{"type":"end_conversation"}` | - -### 服务端返回 - -| 类型 | 说明 | 包含字段 | -|------|------|---------| -| `connect` | 连接确认 | `data.status` | -| `transcript` | 语音转文字 | `data.text` | -| `agent_response` | Agent 回应 | `data.text` | -| `tts_audio` | TTS 音频 | `data.audio_base64` | -| `end_conversation` | 结束确认 | `data.status` | -| `error` | 错误信息 | `data.message` | - ---- - -## 🧪 测试场景 - -### 场景1:基础对话 -1. 连接 → 2. 发送音频 → 3. 接收3条响应 → 4. 结束对话 - -### 场景2:多轮对话 -重复场景1的步骤2-3多次 - -### 场景3:错误处理 -发送无效的 `audio_base64` 数据,查看 `error` 消息 - ---- - -## ⚠️ 常见问题 - -**Q: 连接失败?** -A: 检查后端服务是否运行:`curl http://localhost:8000/health` - -**Q: 收不到响应?** -A: 检查消息格式和必需字段,查看后端日志 - -**Q: 音频处理失败?** -A: 检查音频格式和 Base64 编码,确保 ASR/TTS 服务配置正确 - ---- - -## 📝 完整文档 - -详细测试文档请参考:`WebSocket测试文档.md` diff --git a/api/docs/WebSocket测试文档.md b/api/docs/WebSocket测试文档.md deleted file mode 100644 index e009bc2..0000000 --- a/api/docs/WebSocket测试文档.md +++ /dev/null @@ -1,618 +0,0 @@ -# Life Echo WebSocket 对话接口测试文档 - -## 1. 接口信息 - -### 1.1 连接地址 -``` -ws://localhost:8000/ws/conversation/{conversation_id} -``` - -**参数说明:** -- `conversation_id`: 对话ID(UUID格式,例如:`550e8400-e29b-41d4-a716-446655440000`) - -**示例:** -``` -ws://localhost:8000/ws/conversation/550e8400-e29b-41d4-a716-446655440000 -``` - -### 1.2 协议 -- 协议类型:WebSocket (WS) -- 消息格式:JSON -- 编码:UTF-8 - ---- - -## 2. 消息类型定义 - -### 2.1 客户端 → 服务端消息类型 - -| 消息类型 | 说明 | 必需字段 | -|---------|------|---------| -| `audio_chunk` | 发送音频数据块 | `type`, `data.audio_base64` | -| `end_conversation` | 结束对话 | `type` | - -### 2.2 服务端 → 客户端消息类型 - -| 消息类型 | 说明 | 包含字段 | -|---------|------|---------| -| `connect` | 连接确认 | `type`, `conversation_id`, `data.status`, `timestamp` | -| `transcript` | 语音转文字结果 | `type`, `conversation_id`, `data.text`, `timestamp` | -| `agent_response` | Agent 回应文本 | `type`, `conversation_id`, `data.text`, `timestamp` | -| `tts_audio` | TTS 生成的音频 | `type`, `conversation_id`, `data.audio_base64`, `timestamp` | -| `end_conversation` | 对话结束确认 | `type`, `conversation_id`, `data.status`, `timestamp` | -| `error` | 错误信息 | `type`, `data.message`, `timestamp` | - ---- - -## 3. 消息格式规范 - -### 3.1 客户端消息格式 - -#### 3.1.1 发送音频块 (audio_chunk) -```json -{ - "type": "audio_chunk", - "conversation_id": "550e8400-e29b-41d4-a716-446655440000", - "data": { - "audio_base64": "UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQAAAAA=" - }, - "timestamp": "2024-01-15T10:30:00.000Z" -} -``` - -**字段说明:** -- `type`: 固定值 `"audio_chunk"` -- `conversation_id`: 对话ID(可选,建议包含) -- `data.audio_base64`: Base64编码的音频数据(必需) -- `timestamp`: ISO 8601格式的时间戳(可选) - -#### 3.1.2 结束对话 (end_conversation) -```json -{ - "type": "end_conversation", - "conversation_id": "550e8400-e29b-41d4-a716-446655440000", - "timestamp": "2024-01-15T10:35:00.000Z" -} -``` - -### 3.2 服务端消息格式 - -#### 3.2.1 连接确认 (connect) -```json -{ - "type": "connect", - "conversation_id": "550e8400-e29b-41d4-a716-446655440000", - "data": { - "status": "connected" - }, - "timestamp": "2024-01-15T10:30:00.000Z" -} -``` - -#### 3.2.2 语音转文字结果 (transcript) -```json -{ - "type": "transcript", - "conversation_id": "550e8400-e29b-41d4-a716-446655440000", - "data": { - "text": "我小时候住在北京" - }, - "timestamp": "2024-01-15T10:30:05.000Z" -} -``` - -#### 3.2.3 Agent 回应 (agent_response) -```json -{ - "type": "agent_response", - "conversation_id": "550e8400-e29b-41d4-a716-446655440000", - "data": { - "text": "听起来很有趣!能告诉我更多关于你在北京的生活吗?比如你最喜欢那里的什么地方?" - }, - "timestamp": "2024-01-15T10:30:06.000Z" -} -``` - -#### 3.2.4 TTS 音频 (tts_audio) -```json -{ - "type": "tts_audio", - "conversation_id": "550e8400-e29b-41d4-a716-446655440000", - "data": { - "audio_base64": "UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQAAAAA=" - }, - "timestamp": "2024-01-15T10:30:07.000Z" -} -``` - -#### 3.2.5 对话结束 (end_conversation) -```json -{ - "type": "end_conversation", - "conversation_id": "550e8400-e29b-41d4-a716-446655440000", - "data": { - "status": "ended" - }, - "timestamp": "2024-01-15T10:35:00.000Z" -} -``` - -#### 3.2.6 错误信息 (error) -```json -{ - "type": "error", - "data": { - "message": "ASR服务暂时不可用,请稍后重试" - }, - "timestamp": "2024-01-15T10:30:05.000Z" -} -``` - ---- - -## 4. 在 Apifox 中测试步骤 - -### 4.1 准备工作 - -1. **启动后端服务** - ```bash - cd api - uvicorn main:app --reload --host 0.0.0.0 --port 8000 - ``` - -2. **准备测试数据** - - 生成一个 UUID 作为 `conversation_id`(可以使用在线工具或命令行) - - 准备一段 Base64 编码的音频数据(用于测试) - -### 4.2 创建 WebSocket 连接 - -1. **打开 Apifox** - - 创建新请求 - - 选择协议类型:**WebSocket** - -2. **配置连接地址** - ``` - ws://localhost:8000/ws/conversation/550e8400-e29b-41d4-a716-446655440000 - ``` - - 将 `550e8400-e29b-41d4-a716-446655440000` 替换为你的对话ID - -3. **点击"连接"按钮** - - 连接成功后,应该立即收到 `connect` 类型的消息 - -### 4.3 测试场景 - -#### 场景 1:完整对话流程测试 - -**步骤:** - -1. **连接 WebSocket** - - 地址:`ws://localhost:8000/ws/conversation/{conversation_id}` - - 预期收到:`connect` 消息 - -2. **发送音频块** - ```json - { - "type": "audio_chunk", - "conversation_id": "550e8400-e29b-41d4-a716-446655440000", - "data": { - "audio_base64": "UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQAAAAA=" - } - } - ``` - - 预期收到(按顺序): - 1. `transcript` - 语音转文字结果 - 2. `agent_response` - Agent 回应文本 - 3. `tts_audio` - TTS 生成的音频 - -3. **继续发送多个音频块** - - 重复步骤 2,模拟连续对话 - -4. **结束对话** - ```json - { - "type": "end_conversation", - "conversation_id": "550e8400-e29b-41d4-a716-446655440000" - } - ``` - - 预期收到:`end_conversation` 确认消息 - - 连接自动断开 - -#### 场景 2:错误处理测试 - -**测试无效的音频数据:** -```json -{ - "type": "audio_chunk", - "data": { - "audio_base64": "invalid_base64_data" - } -} -``` -- 预期收到:`error` 消息,包含错误描述 - -**测试缺少必需字段:** -```json -{ - "type": "audio_chunk" -} -``` -- 预期收到:`error` 消息或连接断开 - -#### 场景 3:连接断开测试 - -1. **正常断开** - - 发送 `end_conversation` 消息 - - 连接正常关闭 - -2. **异常断开** - - 直接关闭连接(不发送 `end_conversation`) - - 服务端应清理资源 - -#### 场景 4:多轮对话测试 - -**测试对话阶段切换:** - -1. **发送童年相关话题** - ```json - { - "type": "audio_chunk", - "data": { - "audio_base64": "{童年相关音频的base64}" - } - } - ``` - - 预期:Agent 回应关于童年的话题 - -2. **发送教育相关话题** - ```json - { - "type": "audio_chunk", - "data": { - "audio_base64": "{教育相关音频的base64}" - } - } - ``` - - 预期:Agent 自动切换到教育阶段,回应教育相关话题 - -3. **发送工作相关话题** - - 预期:Agent 切换到职业阶段 - ---- - -## 5. 测试用例清单 - -### 5.1 基础功能测试 - -| 用例ID | 测试场景 | 输入 | 预期结果 | 状态 | -|--------|---------|------|---------|------| -| TC-001 | 建立连接 | 连接 WebSocket | 收到 `connect` 消息 | ⬜ | -| TC-002 | 发送音频块 | 发送 `audio_chunk` | 收到 `transcript`、`agent_response`、`tts_audio` | ⬜ | -| TC-003 | 结束对话 | 发送 `end_conversation` | 收到 `end_conversation` 确认,连接断开 | ⬜ | -| TC-004 | 多轮对话 | 连续发送多个音频块 | 每轮都收到完整响应 | ⬜ | - -### 5.2 异常场景测试 - -| 用例ID | 测试场景 | 输入 | 预期结果 | 状态 | -|--------|---------|------|---------|------| -| TC-101 | 无效音频数据 | 发送无效 base64 | 收到 `error` 消息 | ⬜ | -| TC-102 | 缺少必需字段 | 发送不完整的消息 | 收到 `error` 或连接断开 | ⬜ | -| TC-103 | 无效对话ID | 使用不存在的 conversation_id | 连接成功但创建新对话 | ⬜ | -| TC-104 | 连接超时 | 长时间不发送消息 | 连接保持或超时断开 | ⬜ | -| TC-105 | 异常断开 | 直接关闭连接 | 服务端清理资源 | ⬜ | - -### 5.3 性能测试 - -| 用例ID | 测试场景 | 输入 | 预期结果 | 状态 | -|--------|---------|------|---------|------| -| TC-201 | 快速连续发送 | 1秒内发送10个音频块 | 所有请求都能正常处理 | ⬜ | -| TC-202 | 大音频块 | 发送较大的音频数据 | 正常处理,响应时间在可接受范围 | ⬜ | -| TC-203 | 长时间连接 | 保持连接30分钟 | 连接稳定,无内存泄漏 | ⬜ | - ---- - -## 6. 测试数据准备 - -### 6.1 生成对话ID - -**方法1:使用在线工具** -- 访问:https://www.uuidgenerator.net/ -- 生成 UUID v4 - -**方法2:使用命令行** -```bash -# Python -python -c "import uuid; print(uuid.uuid4())" - -# Node.js -node -e "console.log(require('crypto').randomUUID())" -``` - -### 6.2 准备测试音频 - -**方法1:使用文本转Base64(模拟)** -```bash -# 创建一个简单的测试音频文件(WAV格式) -# 然后转换为Base64 -base64 -i test_audio.wav -``` - -**方法2:使用在线工具** -- 访问:https://base64.guru/converter/encode/audio -- 上传音频文件,获取Base64编码 - -**方法3:使用Python脚本** -```python -import base64 - -with open("test_audio.wav", "rb") as f: - audio_base64 = base64.b64encode(f.read()).decode('utf-8') - print(audio_base64) -``` - -### 6.3 测试音频建议 - -- **格式**:WAV、MP3、M4A -- **时长**:3-10秒 -- **内容**:清晰的中文语音 -- **示例话题**: - - 童年:"我小时候住在北京" - - 教育:"我在清华大学读书" - - 工作:"我在一家科技公司工作" - - 家庭:"我有一个幸福的家庭" - ---- - -## 7. 在 Apifox 中的详细操作 - -### 7.1 创建 WebSocket 请求 - -1. **新建请求** - - 点击"新建" → "WebSocket" - - 或使用快捷键 `Ctrl+N` (Windows) / `Cmd+N` (Mac) - -2. **配置连接** - - **名称**:Life Echo 对话测试 - - **URL**:`ws://localhost:8000/ws/conversation/{conversation_id}` - - **方法**:WebSocket (自动识别) - -3. **添加参数** - - 在 URL 中直接替换 `{conversation_id}` 为实际值 - - 或使用变量:`ws://localhost:8000/ws/conversation/{{conversation_id}}` - -### 7.2 发送消息 - -1. **连接成功后** - - 在消息输入框中输入 JSON 格式的消息 - - 或使用"消息模板" - -2. **创建消息模板** - ```json - { - "type": "audio_chunk", - "conversation_id": "{{conversation_id}}", - "data": { - "audio_base64": "{{audio_base64}}" - }, - "timestamp": "{{$timestamp}}" - } - ``` - -3. **发送消息** - - 点击"发送"按钮 - - 或使用快捷键 `Ctrl+Enter` - -### 7.3 查看响应 - -1. **实时消息** - - 在"消息"面板查看收到的消息 - - 消息按时间顺序显示 - -2. **消息详情** - - 点击消息查看详细信息 - - 可以查看 JSON 格式、时间戳等 - -3. **保存响应** - - 右键消息 → "保存为示例" - - 用于后续对比测试 - -### 7.4 使用环境变量 - -1. **创建环境** - - 点击"环境" → "新建环境" - - 名称:Local Development - -2. **添加变量** - ```json - { - "base_url": "ws://localhost:8000", - "conversation_id": "550e8400-e29b-41d4-a716-446655440000", - "audio_base64": "UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQAAAAA=" - } - ``` - -3. **在请求中使用** - - URL: `{{base_url}}/ws/conversation/{{conversation_id}}` - - 消息中使用 `{{audio_base64}}` - ---- - -## 8. 常见问题排查 - -### 8.1 连接失败 - -**问题**:无法建立 WebSocket 连接 - -**排查步骤:** -1. 检查后端服务是否运行:`curl http://localhost:8000/health` -2. 检查端口是否正确:默认 8000 -3. 检查防火墙设置 -4. 检查 URL 格式是否正确(`ws://` 不是 `http://`) - -### 8.2 收不到响应 - -**问题**:发送消息后没有收到响应 - -**排查步骤:** -1. 检查消息格式是否正确(JSON 格式) -2. 检查必需字段是否包含(`type`, `data.audio_base64`) -3. 检查后端日志是否有错误 -4. 检查 LLM 服务是否配置(DEEPSEEK_API_KEY 或 LLM_API_KEY) - -### 8.3 收到错误消息 - -**问题**:收到 `error` 类型的消息 - -**排查步骤:** -1. 查看错误消息内容:`data.message` -2. 检查 ASR 服务配置(OPENAI_API_KEY) -3. 检查 TTS 服务配置(OPENAI_API_KEY) -4. 检查 LLM 服务配置(DEEPSEEK_API_KEY 或 LLM_API_KEY) - -### 8.4 音频处理失败 - -**问题**:音频转文字失败 - -**排查步骤:** -1. 检查音频格式是否支持(WAV、MP3、M4A) -2. 检查 Base64 编码是否正确 -3. 检查音频文件大小(建议 < 10MB) -4. 检查 ASR 服务是否可用 - ---- - -## 9. 测试检查清单 - -### 9.1 连接测试 -- [ ] 能够成功建立 WebSocket 连接 -- [ ] 连接后立即收到 `connect` 消息 -- [ ] 连接信息包含正确的 `conversation_id` - -### 9.2 消息发送测试 -- [ ] 能够发送 `audio_chunk` 消息 -- [ ] 能够发送 `end_conversation` 消息 -- [ ] 消息格式符合规范 - -### 9.3 消息接收测试 -- [ ] 发送音频后收到 `transcript` 消息 -- [ ] 收到 `agent_response` 消息 -- [ ] 收到 `tts_audio` 消息 -- [ ] 消息顺序正确(transcript → agent_response → tts_audio) - -### 9.4 功能测试 -- [ ] 语音转文字功能正常 -- [ ] Agent 回应内容合理 -- [ ] TTS 音频生成正常 -- [ ] 对话阶段自动切换 - -### 9.5 异常处理测试 -- [ ] 无效消息返回错误 -- [ ] 连接断开时资源清理 -- [ ] 错误消息格式正确 - ---- - -## 10. 性能指标 - -### 10.1 响应时间 - -| 操作 | 目标响应时间 | 实际响应时间 | 状态 | -|------|------------|------------|------| -| 连接建立 | < 1s | - | ⬜ | -| 音频转文字 | < 3s | - | ⬜ | -| Agent 回应 | < 5s | - | ⬜ | -| TTS 生成 | < 3s | - | ⬜ | -| 完整流程 | < 15s | - | ⬜ | - -### 10.2 并发测试 - -- [ ] 支持 10 个并发连接 -- [ ] 支持 50 个并发连接 -- [ ] 支持 100 个并发连接 - ---- - -## 11. 测试报告模板 - -### 测试信息 -- **测试日期**:2024-XX-XX -- **测试人员**:XXX -- **测试环境**:Local Development -- **后端版本**:1.0.0 - -### 测试结果 -- **总用例数**:XX -- **通过数**:XX -- **失败数**:XX -- **通过率**:XX% - -### 问题记录 -1. **问题描述**:XXX - - **严重程度**:高/中/低 - - **复现步骤**:XXX - - **预期结果**:XXX - - **实际结果**:XXX - ---- - -## 12. 附录 - -### 12.1 快速测试脚本 - -**Python 测试脚本示例:** -```python -import asyncio -import websockets -import json -import uuid -import base64 - -async def test_websocket(): - conversation_id = str(uuid.uuid4()) - uri = f"ws://localhost:8000/ws/conversation/{conversation_id}" - - async with websockets.connect(uri) as websocket: - # 接收连接确认 - response = await websocket.recv() - print("收到连接确认:", response) - - # 发送音频块(模拟) - message = { - "type": "audio_chunk", - "conversation_id": conversation_id, - "data": { - "audio_base64": "UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQAAAAA=" - } - } - await websocket.send(json.dumps(message)) - - # 接收响应 - for i in range(3): - response = await websocket.recv() - print(f"收到响应 {i+1}:", response) - - # 结束对话 - end_message = { - "type": "end_conversation", - "conversation_id": conversation_id - } - await websocket.send(json.dumps(end_message)) - - # 接收结束确认 - response = await websocket.recv() - print("收到结束确认:", response) - -asyncio.run(test_websocket()) -``` - -### 12.2 参考链接 - -- FastAPI WebSocket 文档:https://fastapi.tiangolo.com/advanced/websockets/ -- Apifox WebSocket 测试:https://apifox.com/help/app/web-socket -- UUID 生成器:https://www.uuidgenerator.net/ -- Base64 编码工具:https://base64.guru/converter/encode/audio - ---- - -**文档版本**:v1.0 -**最后更新**:2024-01-15 -**维护人员**:Life Echo 开发团队 diff --git a/api/docs/数据迁移指南.md b/api/docs/数据迁移指南.md deleted file mode 100644 index 2e2d37f..0000000 --- a/api/docs/数据迁移指南.md +++ /dev/null @@ -1,258 +0,0 @@ -# 生产环境迁移操作指南 - -本文档描述新表结构(`chapter_sections`、`memoir_images`)的完整迁移步骤,适用于生产环境。 - ---- - -## 一、迁移概览 - -| 阶段 | 内容 | 前置条件 | -|------|------|----------| -| 1 | 创建 `chapter_sections` 表、`chapters.cover_image` 列 | 无 | -| 2 | 将 `chapters.content` + `chapters.images` 迁移到 `chapter_sections`,并删除旧列 | 阶段 1 完成 | -| 3 | 创建 `memoir_images` 表 | 阶段 2 完成(依赖 `chapter_sections`) | -| 4 | 将 `chapters.cover_image`、`chapter_sections.image` 迁移到 `memoir_images` | 阶段 3 完成 | -| 5 | (可选)添加 `chapter_sections.image_id` 外键,删除 `chapter_sections.image` | 阶段 4 完成 | - ---- - -## 二、前置条件检查 - -### 2.1 环境要求 - -- Python 3.x,已安装项目依赖(`uv sync` 或 `pip install -r requirements.txt`) -- 数据库:PostgreSQL -- 环境变量:`.env` 中配置正确的 `DATABASE_URL` - -### 2.2 数据库状态检查 - -执行前确认: - -```sql --- 1. chapters 表存在且包含 content、images 列(迁移前应有) -SELECT column_name FROM information_schema.columns -WHERE table_schema = 'public' AND table_name = 'chapters' -AND column_name IN ('content', 'images', 'cover_image'); --- 预期:content、images 存在;cover_image 可能不存在 - --- 2. chapter_sections 表在阶段 1 前应不存在 -SELECT EXISTS ( - SELECT 1 FROM information_schema.tables - WHERE table_schema = 'public' AND table_name = 'chapter_sections' -); --- 预期:false(首次迁移) - --- 3. memoir_images 表在阶段 3 前应不存在 -SELECT EXISTS ( - SELECT 1 FROM information_schema.tables - WHERE table_schema = 'public' AND table_name = 'memoir_images' -); --- 预期:false(首次迁移) -``` - -### 2.3 备份(强烈建议) - -```bash -# 生产环境务必先备份 -pg_dump -U -d -F c -f backup_$(date +%Y%m%d_%H%M%S).dump -``` - ---- - -## 三、迁移步骤 - -### 阶段 1:执行 SQL 建表/加列(chapter_sections) - -**前置 SQL 条件**:无。`chapters` 表需已存在。 - -**方式 A:使用 psql 手动执行** - -```bash -cd /path/to/life-echo -psql -U -d -f api/migrations/add_chapter_sections.sql -``` - -**方式 B:使用一键脚本(推荐)** - -一键脚本 `run_chapter_sections_migration` 会自动执行该 SQL,然后执行数据迁移,无需单独执行本阶段。 - -若选择方式 A,则需在阶段 2 使用 `migrate_chapters_to_sections`;若选择方式 B,可跳过阶段 1 和 2 的拆分,直接执行 `run_chapter_sections_migration`。 - ---- - -### 阶段 2:数据迁移(chapters → chapter_sections) - -**前置 SQL 条件**:已执行 `api/migrations/add_chapter_sections.sql`,即: - -- `chapter_sections` 表已创建 -- `chapters.cover_image` 列已添加(若不存在会自动添加) - -**执行命令**(在 `api` 目录下): - -```bash -cd /path/to/life-echo/api -uv run python -m scripts.migrate_chapters_to_sections -``` - -或使用一键脚本(包含阶段 1 + 2): - -```bash -cd /path/to/life-echo/api -uv run python -m scripts.run_chapter_sections_migration -``` - -**脚本行为**: - -- 若 `chapters.content` 已不存在,则跳过迁移 -- 将 `chapters.content` + `chapters.images` 解析为 sections,写入 `chapter_sections` -- 将每章首张图写入 `chapters.cover_image` -- 最后删除 `chapters.content`、`chapters.images` - ---- - -### 阶段 3:执行 SQL 建表(memoir_images) - -**前置 SQL 条件**:`chapter_sections` 表已存在(`memoir_images` 的 `section_id` 外键依赖它)。 - -**方式 A:使用 psql 手动执行** - -```bash -cd /path/to/life-echo -psql -U -d -f api/migrations/add_memoir_images_table.sql -``` - -**方式 B:使用 memoir_images 迁移脚本** - -`run_memoir_images_migration` 会先执行该 SQL,再执行数据迁移。若 SQL 文件不存在会给出警告。 - ---- - -### 阶段 4:数据迁移(cover_image / section.image → memoir_images) - -**前置 SQL 条件**:已执行 `api/migrations/add_memoir_images_table.sql`,即 `memoir_images` 表已创建。 - -**执行命令**(在项目根目录或 `api` 目录下): - -```bash -cd /path/to/life-echo -uv run python -m api.scripts.run_memoir_images_migration -``` - -或: - -```bash -cd /path/to/life-echo/api -uv run python -m scripts.run_memoir_images_migration -``` - -**脚本行为**: - -- 先执行 `add_memoir_images_table.sql`(若存在) -- 迁移 `chapters.cover_image` → `memoir_images`(`section_id` 为空表示封面) -- 迁移 `chapter_sections.image` → `memoir_images`(`section_id` 非空表示段落配图) -- 已存在的记录会跳过,支持幂等执行 - ---- - -### 阶段 5:(可选)添加 image_id 外键并删除旧 JSON 列 - -**前置 SQL 条件**:`memoir_images` 表已有数据,且 `chapter_sections` 中对应 section 的配图已迁移完成。 - -**执行**: - -```bash -cd /path/to/life-echo -psql -U -d -f api/migrations/add_section_image_id_fk.sql -``` - -**该 SQL 会**: - -1. 为 `chapter_sections` 添加 `image_id` 列(外键指向 `memoir_images`) -2. 回填:将 `memoir_images` 中 `section_id` 指向本行的记录的 `id` 写入 `image_id` -3. 删除 `chapter_sections.image` 列 - ---- - -## 四、推荐生产执行顺序(精简版) - -### 方案 A:分步执行(便于排查问题) - -```bash -# 1. 备份 -pg_dump -U -d -F c -f backup_$(date +%Y%m%d).dump - -# 2. chapter_sections 迁移(SQL + 数据) -cd /path/to/life-echo/api -psql -U -d -f migrations/add_chapter_sections.sql -uv run python -m scripts.migrate_chapters_to_sections - -# 3. memoir_images 迁移(SQL + 数据) -cd /path/to/life-echo -psql -U -d -f api/migrations/add_memoir_images_table.sql -uv run python -m api.scripts.run_memoir_images_migration - -# 4. (可选)image_id 外键 -psql -U -d -f api/migrations/add_section_image_id_fk.sql -``` - -### 方案 B:使用一键脚本(更简洁) - -```bash -# 1. 备份 -pg_dump -U -d -F c -f backup_$(date +%Y%m%d).dump - -# 2. chapter_sections 一键迁移(内含 SQL + 数据) -cd /path/to/life-echo/api -uv run python -m scripts.run_chapter_sections_migration - -# 3. memoir_images 一键迁移(内含 SQL + 数据) -cd /path/to/life-echo -uv run python -m api.scripts.run_memoir_images_migration - -# 4. (可选)image_id 外键 -psql -U -d -f api/migrations/add_section_image_id_fk.sql -``` - ---- - -## 五、前置 SQL 条件汇总 - -| 脚本 / SQL 文件 | 前置条件 | -|-----------------|----------| -| `add_chapter_sections.sql` | `chapters` 表已存在 | -| `migrate_chapters_to_sections` / `run_chapter_sections_migration` | 已执行 `add_chapter_sections.sql` | -| `add_memoir_images_table.sql` | `chapter_sections` 表已存在 | -| `run_memoir_images_migration` | 已执行 `add_memoir_images_table.sql`(或脚本会自动执行) | -| `add_section_image_id_fk.sql` | `memoir_images` 表已存在且数据已迁移完成 | - ---- - -## 六、回滚说明 - -- 迁移会删除 `chapters.content`、`chapters.images`,以及(若执行阶段 5)`chapter_sections.image` -- 回滚需从备份恢复,或根据业务自行编写反向迁移脚本 -- 建议在测试环境完整跑通后再在生产执行 - ---- - -## 七、验证 - -```sql --- 1. chapter_sections 有数据 -SELECT COUNT(*) FROM chapter_sections; - --- 2. chapters 无 content/images -SELECT column_name FROM information_schema.columns -WHERE table_schema = 'public' AND table_name = 'chapters' -AND column_name IN ('content', 'images'); --- 预期:无结果 - --- 3. memoir_images 有数据 -SELECT COUNT(*) FROM memoir_images; - --- 4. 封面与段落配图数量合理 -SELECT - COUNT(*) FILTER (WHERE section_id IS NULL) AS cover_count, - COUNT(*) FILTER (WHERE section_id IS NOT NULL) AS section_count -FROM memoir_images; -``` diff --git a/api/docs/文字交流模式说明.md b/api/docs/文字交流模式说明.md deleted file mode 100644 index 1869919..0000000 --- a/api/docs/文字交流模式说明.md +++ /dev/null @@ -1,213 +0,0 @@ -# 文字交流模式说明 - -## 概述 - -当前系统已配置为**纯文字交流模式**,暂时不接通语音模块(ASR/TTS)。用户可以通过WebSocket发送文字消息与AI进行对话,系统会自动将对话记录整理成书的多个章节。 - -## 功能特性 - -### 1. 智能对话引导 -- 用户通过WebSocket发送 `TEXT` 类型的消息 -- AI Agent 基于人生阶段(童年、教育、事业、家庭、信念)智能引导 -- Agent 可能返回 1-3 条连续消息,像微信聊天一样自然 -- 所有对话记录保存到数据库 - -### 2. 回忆录增量生成 -- **实时处理**:每条用户消息都会触发后台异步分析 -- **智能分类**:自动识别内容所属的人生阶段 -- **增量写入**:新内容追加到对应章节,不覆盖已有内容 -- **创意标题**:自动生成富有文学性的章节标题 - -### 3. 共享状态管理 -- 对话 Agent 和回忆录 Agent 共享状态 -- 状态包含:当前阶段、已完成阶段、各阶段已收集的信息片段 -- 对话 Agent 根据状态智能选择下一个问题方向 - -### 4. 用户认证 -- 所有操作需要用户登录 -- 使用JWT访问令牌进行认证 -- WebSocket连接需要传递token参数 - -## WebSocket 消息格式 - -### 客户端发送 - -#### 文字消息 -```json -{ - "type": "text", - "conversation_id": "conversation-id", - "data": { - "text": "我小时候住在北京" - }, - "timestamp": "2024-01-15T10:30:00.000Z" -} -``` - -#### 结束对话 -```json -{ - "type": "end_conversation", - "conversation_id": "conversation-id" -} -``` - -### 服务端返回 - -#### 连接确认 -```json -{ - "type": "connect", - "conversation_id": "conversation-id", - "data": { - "status": "connected" - }, - "timestamp": "2024-01-15T10:30:00.000Z" -} -``` - -#### Agent 回应 -```json -{ - "type": "agent_response", - "conversation_id": "conversation-id", - "data": { - "text": "听起来很有趣!能告诉我更多关于你在北京的生活吗?", - "index": 0, - "total": 1 - }, - "timestamp": "2024-01-15T10:30:05.000Z" -} -``` - -**多消息支持**:Agent 可能返回多条消息(最多3条),通过 `index` 和 `total` 字段标识: -- `index`: 当前消息序号(从0开始) -- `total`: 本次回复的总消息数 - -示例(Agent 返回2条消息): -```json -// 第1条 -{"type": "agent_response", "data": {"text": "那个小院子听起来很温馨。", "index": 0, "total": 2}} -// 第2条(约0.5秒后) -{"type": "agent_response", "data": {"text": "奶奶给你讲的故事里,有没有哪个让你印象特别深刻?", "index": 1, "total": 2}} -``` - -#### 对话结束确认 -```json -{ - "type": "end_conversation", - "conversation_id": "conversation-id", - "data": { - "status": "ended" - }, - "timestamp": "2024-01-15T10:35:00.000Z" -} -``` - -## 使用流程 - -### 1. 用户注册/登录 -```bash -POST /api/auth/register -{ - "phone": "13800138000", - "password": "password123", - "nickname": "用户昵称" -} - -# 返回 access_token 和 refresh_token -``` - -### 2. 创建对话 -```bash -POST /api/conversations -Authorization: Bearer {access_token} - -# 返回 conversation_id -``` - -### 3. 连接 WebSocket -``` -ws://localhost:8000/ws/conversation/{conversation_id}?token={access_token} -``` - -### 4. 发送文字消息 -发送 `TEXT` 类型消息,接收 `AGENT_RESPONSE` 回应 - -### 5. 结束对话 -发送 `END_CONVERSATION` 消息,系统自动整理章节 - -### 6. 查看章节 -```bash -GET /api/chapters -Authorization: Bearer {access_token} - -# 仅查看未读章节 -GET /api/chapters?is_new=true -Authorization: Bearer {access_token} -``` - -### 7. 查看回忆录状态 -```bash -GET /api/memoir-state -Authorization: Bearer {access_token} - -# 返回示例 -{ - "stage_order": ["childhood", "education", "career", "family", "belief"], - "current_stage": "childhood", - "covered_stages": [], - "slots": { - "childhood": { - "place": {"snippet": "在南方一个小镇长大"}, - "people": {"snippet": "跟奶奶关系很好"}, - ... - } - } -} -``` - -### 8. 获取下一个问题方向 -```bash -GET /api/memoir-state/next-question -Authorization: Bearer {access_token} - -# 返回当前阶段和尚未覆盖的话题 -{ - "current_stage": "childhood", - "empty_slots": ["daily_life", "turning_event"], - "covered_stages": [] -} -``` - -### 9. 标记回忆录已读 -```bash -POST /api/memoir-state/mark-read -Authorization: Bearer {access_token} -``` - -## 已移除的功能 - -- ❌ 音频块处理(`AUDIO_CHUNK`) -- ❌ 语音转文字(ASR) -- ❌ 文字转语音(TTS) -- ❌ 转写结果消息(`TRANSCRIPT`) -- ❌ TTS音频消息(`TTS_AUDIO`) - -## 保留的功能 - -- ✅ 文字消息处理(`TEXT`) -- ✅ Agent 多消息回应(`AGENT_RESPONSE`,支持连续 1-3 条) -- ✅ 智能对话阶段引导 -- ✅ 回忆录增量生成(实时后台处理) -- ✅ 共享状态管理(回忆录状态 API) -- ✅ 用户认证 -- ✅ 对话管理 - -## 注意事项 - -1. **WebSocket 连接必须提供 token**:`ws://.../ws/conversation/{id}?token={access_token}` -2. **所有对话记录都会保存**:用于后续章节整理 -3. **章节增量生成**:每条消息都会触发后台异步分析,不必等对话结束 -4. **章节按类别自动分类**:系统会根据内容自动判断章节类别 -5. **多消息处理**:Agent 可能返回多条消息,客户端应根据 `index` 和 `total` 字段正确处理 diff --git a/api/docs/测试脚本使用说明.md b/api/docs/测试脚本使用说明.md deleted file mode 100644 index 6fb864e..0000000 --- a/api/docs/测试脚本使用说明.md +++ /dev/null @@ -1,207 +0,0 @@ -# 测试脚本使用说明 - -## 概述 - -`test_conversation.py` 是一个自动化测试脚本,用于测试 Life Echo 的完整对话流程,包括: -- 用户注册/登录 -- WebSocket 多轮对话 -- 回忆录状态检查 -- 章节生成验证 - -## 前置条件 - -### 1. 环境准备 - -```bash -cd api - -# 安装依赖 -pip install -r requirements.txt - -# 确保 httpx 已安装(测试脚本需要) -pip install httpx -``` - -### 2. 配置文件 - -确保 `.env` 文件包含以下配置: - -```env -# LLM 配置(必需) -DEEPSEEK_API_KEY=your-api-key -DEEPSEEK_BASE_URL=https://api.deepseek.com -DEEPSEEK_MODEL=deepseek-chat - -# JWT 配置(必需) -SECRET_KEY=your-secret-key -ALGORITHM=HS256 -ACCESS_TOKEN_EXPIRE_MINUTES=120 - -# 数据库(使用 PostgreSQL,与线上一致) -DATABASE_URL=postgresql://postgres:postgres@localhost:5432/life_echo -``` - -### 3. 启动服务器 - -在一个终端窗口中启动服务器: - -```bash -cd api -uvicorn main:app --reload --host 0.0.0.0 --port 8000 -``` - -验证服务器运行: -```bash -curl http://localhost:8000/health -# 预期返回: {"status":"ok"} -``` - -## 运行测试 - -### 基本用法 - -在另一个终端窗口中运行: - -```bash -cd api -python test_conversation.py -``` - -## 测试流程 - -脚本会自动执行以下步骤: - -### 1. 用户注册 -- 生成随机手机号进行注册 -- 获取 JWT access_token - -### 2. 查看初始状态 -- 调用 `/api/memoir-state` 获取初始回忆录状态 -- 显示当前阶段(默认 `childhood`) - -### 3. 多轮对话(7轮) -- 建立 WebSocket 连接 -- 发送预设的对话内容(涵盖童年和教育阶段) -- 接收 Agent 回复(支持多条消息) - -### 4. 结束对话 -- 发送 `end_conversation` 消息 -- 等待对话结束确认 - -### 5. 检查结果(等待60秒后台处理) -- 查看回忆录状态(slots 填充情况) -- 查看生成的章节列表 -- 显示回忆录完整内容 - -## 输出示例 - -``` -============================================================ -🎭 Life Echo 多轮对话测试 -⏰ 开始时间: 2026-01-21 22:07:22 -============================================================ - -📝 注册用户: 138c41775d5 -✅ 注册成功! -🔑 Token: eyJhbGciOiJIUzI1NiIsInR5cCI6Ik... - -📋 初始回忆录状态: - 当前阶段: childhood - -🔗 连接 WebSocket: 2f83d622-6391-46fb-8caa-b7237bf9e26b -✅ 连接成功: connect - -============================================================ -📤 第 1 轮对话 -============================================================ -👤 用户: 我出生在南方一个小镇,小时候跟奶奶住在一起。 -🤖 Agent: 那个小镇听起来很温馨。 -🤖 Agent: 和奶奶一起住的时候,你印象最深的是什么? - -... (更多对话) - -============================================================ -📊 检查结果 -============================================================ - -⏳ 等待后台处理完成... - 等待中... 10秒 - 等待中... 20秒 - ... - -📋 回忆录状态: - 当前阶段: education - 已完成阶段: ['childhood'] - childhood 已填充: ['place', 'people', 'emotion'] - - place: 在南方一个小镇长大... - - people: 跟奶奶关系很好... - -📚 生成的章节: - 🆕 [childhood] 奶奶院子里的夏天 - 🆕 [education] 第一次离开家的少年 - -📖 回忆录信息: - 标题: 我的回忆录 - 总字数: 0 - 有更新: 是 - -============================================================ -📜 回忆录完整内容 -============================================================ - -────────────────────────────────────────────────────────────── -【奶奶院子里的夏天】 -────────────────────────────────────────────────────────────── -我出生在南方一个小镇,小时候跟奶奶住在一起。奶奶家有个小院子... - -============================================================ -⏰ 结束时间: 2026-01-21 22:09:30 -============================================================ -``` - -## 自定义测试内容 - -修改 `test_conversation.py` 中的 `CONVERSATION_MESSAGES` 列表来测试不同的对话场景: - -```python -CONVERSATION_MESSAGES = [ - # 童年阶段 - "我出生在南方一个小镇,小时候跟奶奶住在一起。", - "奶奶家有个小院子,夏天的时候我们经常坐在院子里乘凉。", - - # 教育阶段 - "后来我去城里上学了,那是我第一次离开家。", - - # 事业阶段 - "大学毕业后我进了一家互联网公司...", - - # ... 添加更多对话 -] -``` - -## 常见问题 - -### Q: 测试超时失败? - -A: 后台 LLM 调用可能较慢,请检查: -1. 网络连接是否正常 -2. API Key 是否有效 -3. 可以增加等待时间(修改 `check_results` 中的等待逻辑) - -### Q: 章节未生成? - -A: 检查以下情况: -1. 查看服务器日志是否有错误 -2. 后台处理有 5 秒去抖延迟 + LLM 调用时间,需要等待 - -### Q: Agent 回复包含括号注释? - -A: 这是 LLM 的问题,已在 prompt 中明确禁止。如果仍然出现,可以: -1. 检查 `agents/prompts/conversation_prompts.py` 中的禁止规则 -2. 考虑在后处理中过滤 - -## 相关文件 - -- `test_conversation.py` - 测试脚本 -- `docs/文字交流模式说明.md` - API 接口说明 -- `docs/WebSocket测试文档.md` - WebSocket 详细测试文档 diff --git a/api/docs/短信验证码测试指南.md b/api/docs/短信验证码测试指南.md deleted file mode 100644 index a3ac637..0000000 --- a/api/docs/短信验证码测试指南.md +++ /dev/null @@ -1,331 +0,0 @@ -# 短信验证码功能测试指南 - -## 概述 - -本文档说明如何测试短信验证码相关功能,包括发送验证码、验证码注册、验证码登录、密码重置、修改密码、修改手机号和登出管理等功能。 - -## 测试脚本 - -测试脚本位于:`api/test_sms_verification.py` - -## 使用方法 - -### 1. 启动后端服务 - -```bash -cd api -python main.py -``` - -确保后端服务运行在 `http://localhost:8000` - -### 2. 运行测试脚本 - -```bash -cd api -python test_sms_verification.py -``` - -### 3. 选择测试模式 - -脚本提供三种测试模式: - -#### 模式1:交互式测试(推荐) - -- 需要真实的短信验证码 -- 适合完整的端到端测试 -- 步骤: - 1. 输入测试手机号 - 2. 脚本发送验证码到手机 - 3. 输入收到的验证码 - 4. 按提示完成各项测试 - -#### 模式2:自动化测试 - -- 需要后端支持测试验证码(如:123456) -- 适合快速回归测试 -- 测试频率限制等功能 - -#### 模式3:API连接测试 - -- 仅测试API是否可访问 -- 适合快速检查服务状态 - -## 测试用例 - -### 1. 发送验证码 - -**测试场景:** -- 发送注册验证码 -- 发送登录验证码 -- 发送重置密码验证码 -- 发送修改手机号验证码 - -**验证点:** -- ✓ 验证码成功发送 -- ✓ 返回过期时间(300秒) -- ✓ 短信成功送达 - -### 2. 频率限制 - -**测试场景:** -- 60秒内重复发送验证码 - -**验证点:** -- ✓ 第一次发送成功 -- ✓ 60秒内第二次发送被拒绝(HTTP 429) -- ✓ 返回剩余等待时间 - -### 3. 验证码注册 - -**测试场景:** -- 使用验证码完成注册 - -**验证点:** -- ✓ 验证码验证成功 -- ✓ 用户创建成功 -- ✓ 返回访问令牌 -- ✓ 返回刷新令牌 - -### 4. 验证码登录 - -**测试场景:** -- 使用验证码登录已存在的账号 - -**验证点:** -- ✓ 验证码验证成功 -- ✓ 返回访问令牌 -- ✓ 可以访问受保护的API - -### 5. 密码重置 - -**测试场景:** -- 忘记密码,使用验证码重置 - -**验证点:** -- ✓ 验证码验证成功 -- ✓ 密码更新成功 -- ✓ 可以使用新密码登录 - -### 6. 修改密码(已登录) - -**测试场景:** -- 已登录用户修改密码 - -**验证点:** -- ✓ 旧密码验证成功 -- ✓ 新密码更新成功 -- ✓ 需要重新登录 - -### 7. 修改手机号 - -**测试场景:** -- 已登录用户修改手机号 - -**验证点:** -- ✓ 新手机号验证码验证成功 -- ✓ 手机号更新成功 -- ✓ 用户信息中手机号已更新 - -### 8. 登出所有设备 - -**测试场景:** -- 登出所有已登录的设备 - -**验证点:** -- ✓ 所有刷新令牌失效 -- ✓ 当前访问令牌仍然有效(直到过期) -- ✓ 无法使用刷新令牌获取新的访问令牌 - -## 手动测试步骤 - -### 测试注册流程 - -1. 打开Android应用,进入注册页面 -2. 输入手机号:`13800138000` -3. 点击"发送验证码"按钮 -4. 查看手机短信,输入6位验证码 -5. 输入密码、昵称等信息 -6. 点击"注册"按钮 -7. 验证是否自动登录并跳转到主页 - -### 测试登录流程 - -1. 打开Android应用,进入登录页面 -2. 切换到"验证码登录"模式 -3. 输入手机号 -4. 点击"发送验证码"按钮 -5. 输入收到的验证码 -6. 点击"登录"按钮 -7. 验证是否成功登录 - -### 测试密码重置流程 - -1. 在登录页面点击"忘记密码" -2. 输入手机号 -3. 点击"发送验证码" -4. 输入收到的验证码 -5. 输入新密码并确认 -6. 点击"重置密码" -7. 验证是否跳转到登录页面 -8. 使用新密码登录验证 - -### 测试账户管理 - -1. 登录后进入"个人"页面 -2. 点击"设置" -> "账户管理" -3. 测试修改密码功能 -4. 测试修改手机号功能 -5. 测试登出当前设备 -6. 测试登出所有设备 - -## 常见问题 - -### Q1: 收不到短信验证码? - -**可能原因:** -- 腾讯云短信服务未配置 -- 手机号未在白名单中(测试环境) -- 短信发送失败(查看后端日志) -- 手机号格式错误 - -**解决方法:** -- 检查 `.env.production` 中的短信配置 -- 查看后端日志确认发送状态 -- 确认手机号格式正确(11位数字) - -### Q2: 验证码总是提示错误? - -**可能原因:** -- 验证码已过期(5分钟) -- 验证码已被使用 -- 验证码输入错误 - -**解决方法:** -- 重新发送验证码 -- 确认输入的验证码正确 -- 检查数据库中的验证码记录 - -### Q3: 频率限制不生效? - -**可能原因:** -- Redis未配置 -- 频率限制逻辑未实现 - -**解决方法:** -- 确认Redis服务运行正常 -- 检查后端频率限制代码 - -### Q4: 登出所有设备后仍然可以访问API? - -**说明:** -- 这是正常行为 -- 访问令牌(Access Token)在过期前仍然有效 -- 刷新令牌(Refresh Token)已失效,无法获取新的访问令牌 -- 等待访问令牌过期后,将无法访问受保护的API - -## 测试检查清单 - -- [ ] 发送验证码成功 -- [ ] 收到短信验证码 -- [ ] 频率限制生效(60秒内不能重复发送) -- [ ] 验证码注册成功 -- [ ] 验证码登录成功 -- [ ] 密码重置成功 -- [ ] 修改密码成功 -- [ ] 修改手机号成功 -- [ ] 登出当前设备成功 -- [ ] 登出所有设备成功 -- [ ] 验证码过期后无法使用 -- [ ] 验证码使用后无法重复使用 -- [ ] 错误的验证码无法通过验证 - -## 性能测试 - -### 并发测试 - -使用 Apache Bench 或 JMeter 测试: - -```bash -# 测试发送验证码接口 -ab -n 100 -c 10 -p sms_request.json -T application/json http://localhost:8000/api/auth/sms/send -``` - -**验证点:** -- 响应时间 < 1秒 -- 成功率 > 99% -- 频率限制正确生效 - -### 数据库性能 - -**验证点:** -- 验证码查询使用索引 -- 过期验证码定期清理 -- 数据库连接池正常 - -## 安全测试 - -### 测试验证码安全性 - -1. 尝试使用已过期的验证码 -2. 尝试使用已使用的验证码 -3. 尝试使用错误的验证码 -4. 尝试暴力破解验证码(应被频率限制阻止) -5. 尝试使用其他手机号的验证码 - -### 测试密码安全性 - -1. 尝试使用弱密码(应被拒绝) -2. 验证密码是否加密存储 -3. 验证旧密码验证逻辑 - -## 日志检查 - -测试时应检查以下日志: - -1. **短信发送日志** - - 发送时间 - - 手机号 - - 发送结果 - - 错误信息(如有) - -2. **验证码验证日志** - - 验证时间 - - 手机号 - - 验证结果 - - 失败原因(如有) - -3. **用户操作日志** - - 注册/登录时间 - - IP地址 - - 设备信息 - -## 监控指标 - -生产环境应监控: - -1. **短信发送成功率** - - 目标:> 99% - -2. **验证码验证成功率** - - 目标:> 95% - -3. **API响应时间** - - 发送验证码:< 1秒 - - 验证验证码:< 500ms - -4. **频率限制触发次数** - - 监控异常行为 - -## 总结 - -完成以上所有测试后,确认: - -1. ✓ 所有API端点正常工作 -2. ✓ 短信验证码正常发送和验证 -3. ✓ 频率限制正确生效 -4. ✓ 安全机制正常工作 -5. ✓ Android应用集成正常 -6. ✓ 用户体验流畅 - -如有问题,请查看后端日志或联系开发团队。 diff --git a/docs/AGENT.md b/docs/AGENT.md deleted file mode 100644 index 9540a1f..0000000 --- a/docs/AGENT.md +++ /dev/null @@ -1,249 +0,0 @@ -# 《岁月时书》MVP(PRD 2.0) - -一定要使用 docs/color.png 的配色(不使用渐变),界面简洁,交互优雅 - -## 0. 设计原则 - -* **微信式聊天体验**:熟悉的交互,降低学习成本 -* **多模态输入**:文字、语音、图片、表情 -* **AI 引导对话**:提问、陪伴、引导,最终目的是完成回忆录 -* **结果导向**:用户随时能看到"书在变厚" - ---- - -## 1. 配色方案(docs/color.png) - -``` -#200028 - 深紫色(文字主色) -#8C8EA3 - 石板紫(次要文字、占位符) -#A177A6 - 中紫色(主题色、按钮、头部背景) -#CEB0DA - 薰衣草色(头像背景、高亮) -#DBBABA - 腮红色(用户头像) -#FAF7F8 - 奶油色(页面背景) -#FFFFFF - 白色(卡片、气泡) -``` - -**注意:不使用渐变色,使用纯色** - ---- - -## 2. 信息架构 - -```text -[对话列表] chat-list.html <——主入口(默认首页) - ├─ 对话列表(联系人) - ├─ 底部导航栏 - └─ 点击进入聊天详情 - -[聊天详情] chat.html <——全屏聊天(微信风格) - ├─ 顶部:返回按钮 + 联系人名称 - ├─ 消息区域(AI白色气泡/用户紫色气泡) - ├─ 输入区域(文字/语音/表情/图片) - └─ 无底部导航(全屏沉浸) - -[我的回忆录] demo.html > 回忆录tab <——阅读页(电子书体验) - ├─ 目录(章节) - ├─ 章节正文(排版优雅+插图) - └─ 导出 PDF / 分享链接(付费口) - -[我的] demo.html > 我的tab <——个人主页 - ├─ 套餐与付费 - ├─ 数据与隐私 - ├─ 设置 - └─ 帮助 -``` - ---- - -## 3. 页面 1:对话列表(chat-list.html) - -### 3.1 页面目标 - -* 让用户 **一眼看到对话入口** -* 微信式联系人列表体验 -* 显示最近消息预览 - -### 3.2 核心组件(从上到下) - -**A. 顶部头部** - -* 应用标题:`岁月时书` -* 副标题:`用对话,留住珍贵的记忆` -* 背景色:中紫色 `#A177A6` - -**B. 对话列表** - -* 联系人头像(圆角方形,薰衣草色背景) -* 联系人名称:`回忆录助手` -* 最新消息预览(单行省略) -* 时间戳 - -**C. 小贴士卡片** - -* 引导用户了解产品用途 - -**D. 底部导航栏** - -* 对话(当前) -* 回忆录 -* 我的 - -### 3.3 功能清单 - -* [x] 显示对话列表 -* [x] 点击进入聊天详情 -* [x] 底部导航切换页面 - ---- - -## 4. 页面 2:聊天详情(chat.html) - -### 4.1 页面目标 - -* **全屏沉浸式聊天**,无底部导航干扰 -* **微信式交互**:熟悉的聊天体验 -* **AI 主动引导**:提问、追问、陪伴 - -### 4.2 核心组件(从上到下) - -**A. 顶部导航栏** - -* 返回按钮(返回对话列表) -* 联系人名称:`回忆录助手` -* 在线状态 -* 背景色:中紫色 `#A177A6` - -**B. 消息区域** - -* AI 消息:白色气泡,左侧显示,薰衣草色头像 `#CEB0DA` -* 用户消息:紫色气泡 `#A177A6`,右侧显示,腮红色头像 `#DBBABA` -* 时间戳(居中显示) -* 支持:文字、图片、语音消息 - -**C. 输入区域** - -* 语音/键盘切换按钮 -* 文字输入框 / 按住说话按钮 -* 表情选择器 -* 更多选项(照片、拍摄) -* 发送按钮(有文字时显示,中紫色背景) - -**D. 扩展面板** - -* 表情选择器:常用表情 32 个 -* 更多选项:照片、拍摄 - -### 4.3 AI 对话逻辑 - -* 用户发送消息后,AI 显示"正在输入..." -* AI 回复引导性问题,帮助用户回忆 -* 示例回复: - - "这个故事真让人感动!能再详细说说当时的场景吗?" - - "那个时候您多大年纪?身边有哪些人陪着您?" - - "您有没有保留那个时候的照片或者物件?" - - "这段记忆很宝贵呢。后来这件事有什么后续吗?" - -### 4.4 功能清单 - -* [x] 文字消息发送/接收 -* [x] 语音消息(按住说话,上滑取消) -* [x] 图片消息 -* [x] 表情输入 -* [x] AI 自动回复(引导式提问) -* [x] 返回对话列表 - ---- - -## 5. 页面 3:我的回忆录(阅读电子书体验) - -### 5.1 页面目标 - -* 打开就是"书"的感觉:**优雅排版 + 章节清晰 + 可插图** -* 能让用户产生:**"真的成书了"** 的成就感 -* 付费/导出在这里完成(不打扰阅读) - -### 5.2 阅读体验 - -**A. 目录页(默认进入)** - -* 书名 + 副标题 -* 章节列表(带进度) -* "最近更新"提示 - -**B. 章节阅读页** - -* 章节标题(大号) -* 段落排版:行距舒适、首行缩进 -* 引用块:把金句提出来 -* 插图展示 - -**C. 书内工具条** - -* 目录 -* 导出 PDF(付费点) -* 分享链接(付费点) - -### 5.3 功能清单 - -* [x] 目录 + 章节阅读 -* [x] 章节内容展示 -* [x] 显示插图 -* [x] 导出 PDF 入口 -* [x] 生成分享链接入口 - ---- - -## 6. 页面 4:我的(个人主页) - -### 6.1 页面目标 - -* 放所有"非核心"功能:设置、付费、客服、隐私 -* 不打扰聊天主路径 - -### 6.2 模块结构 - -**A. 账户卡片** - -* 头像/昵称 -* 当前套餐状态 - -**B. 套餐与付费** - -* 升级套餐 -* 我的订单 - -**C. 数据与隐私** - -* 导出所有数据 - -**D. 设置** - -* 语速 -* 大字模式 -* 夜间模式 -* 每日提醒 - -**E. 帮助** - -* 常见问题 -* 反馈与客服 -* 关于我们 - ---- - -## 7. MVP 版本范围 - -### 必做 - -* [] 对话列表页面 -* [] 全屏聊天页面 -* [] 文字/语音/图片/表情输入 -* [] AI 自动回复 -* [] 微信风格界面 -* [ ] 真实 AI 对接 -* [ ] 语音识别 -* [ ] 图片上传到服务器 -* [ ] 聊天记录持久化 -* [ ] 回忆录自动生成,在线阅读 -* [ ] 用户登录/注册 -* [ ] 套餐购买 diff --git a/docs/SMS_VERIFICATION_IMPLEMENTATION_STATUS.md b/docs/SMS_VERIFICATION_IMPLEMENTATION_STATUS.md deleted file mode 100644 index 05198db..0000000 --- a/docs/SMS_VERIFICATION_IMPLEMENTATION_STATUS.md +++ /dev/null @@ -1,158 +0,0 @@ -# 腾讯云短信验证码集成实施状态 - -## ✅ 已完成的任务 - -### 后端实施(FastAPI) - -1. **环境配置与依赖** ✅ - - 已在 `api/requirements.txt` 中添加 `tencentcloud-sdk-python>=3.0.1000` - - 已在 `api/.env.production` 中添加腾讯云短信配置项 - -2. **数据库模型扩展** ✅ - - 已在 `api/database/models.py` 中添加 `SmsVerificationCode` 模型 - - 已扩展 `RefreshToken` 表,添加 `device_info` 字段 - -3. **短信服务实现** ✅ - - 已创建 `api/services/sms_service.py` - - 实现了验证码生成、发送、验证和频率限制功能 - - 集成了腾讯云SMS SDK - -4. **认证路由扩展** ✅ - - 已在 `api/routers/auth.py` 中添加以下端点: - - `POST /api/auth/sms/send` - 发送验证码 - - `POST /api/auth/login/sms` - 验证码登录 - - `POST /api/auth/register/sms` - 验证码注册 - - `POST /api/auth/password/reset` - 重置密码 - - `POST /api/auth/password/change` - 修改密码 - - `POST /api/auth/phone/change` - 修改手机号 - - `POST /api/auth/logout/all` - 登出所有设备 - -5. **数据库迁移脚本** ✅ - - 已创建 `api/migrations/add_sms_verification.sql` - -### Android端实施 - -6. **数据模型** ✅ - - 已在 `AuthModels.kt` 中添加所有短信验证相关的请求和响应模型 - -7. **网络服务** ✅ - - 已在 `AuthService.kt` 中添加所有短信验证相关的API调用方法 - -8. **ViewModel扩展** ✅ - - 已在 `AuthViewModel.kt` 中添加: - - 验证码倒计时状态管理 - - 发送验证码、验证码登录/注册、重置密码、修改密码、修改手机号、登出所有设备等方法 - -9. **UI组件** ✅ - - 已创建 `SmsCodeInput.kt` - 验证码输入框组件 - - 已创建 `SendSmsButton.kt` - 发送验证码按钮组件 - - 已创建 `PasswordStrengthIndicator.kt` - 密码强度指示器组件 - -10. **注册页面** ✅ - - 已修改 `RegisterScreen.kt`,添加验证码验证流程 - - 集成了验证码输入、发送按钮和密码强度指示器 - -## 🔄 进行中/待完成的任务 - -### Android端UI实施 - -11. **登录页面** 🔄 - - 需要修改 `LoginScreen.kt` - - 添加密码登录和验证码登录的切换功能 - - 添加"忘记密码"链接 - -12. **密码重置页面** ⏳ - - 需要创建 `ResetPasswordScreen.kt` - - 实现通过验证码重置密码的完整流程 - -13. **账户管理页面** ⏳ - - 需要创建 `AccountManagementScreen.kt` - - 实现修改密码、修改手机号、登出管理功能 - -14. **个人页面修改** ⏳ - - 需要修改 `ProfileScreen.kt` - - 添加"账户管理"入口 - - 移除原有的登出按钮(归并到账户管理中) - -15. **导航配置** ⏳ - - 需要更新 `AppNavigation.kt` - - 添加密码重置页面和账户管理页面的路由 - -### 测试与部署 - -16. **测试** ⏳ - - 创建后端测试脚本 `api/test_sms_verification.py` - - 进行端到端测试 - -17. **部署** ⏳ - - 配置生产环境的腾讯云短信服务凭证 - - 执行数据库迁移脚本 - - 更新 `docker-compose.yml` 添加环境变量 - -## 📝 下一步操作指南 - -### 1. 完成剩余的Android UI页面 - -#### 修改登录页面 -```kotlin -// 在 LoginScreen.kt 中添加: -// 1. 登录方式切换(密码/验证码) -// 2. 验证码登录UI -// 3. "忘记密码"链接 -``` - -#### 创建密码重置页面 -```kotlin -// 创建 ResetPasswordScreen.kt -// 实现:手机号 → 验证码 → 新密码 → 确认密码 → 提交 -``` - -#### 创建账户管理页面 -```kotlin -// 创建 AccountManagementScreen.kt -// 包含:修改密码、修改手机号、登出当前设备、登出所有设备 -``` - -### 2. 配置腾讯云短信服务 - -1. 登录腾讯云控制台 -2. 开通短信服务 -3. 创建应用并获取 SDK AppID -4. 配置签名和模板: - - 注册验证码模板 - - 登录验证码模板 - - 重置密码验证码模板 - - 修改手机号验证码模板 -5. 获取 SecretId 和 SecretKey -6. 更新 `api/.env.production` 文件中的配置 - -### 3. 执行数据库迁移 - -```bash -cd api -psql -U postgres -d life_echo -f migrations/add_sms_verification.sql -``` - -### 4. 测试验证 - -1. 后端测试: - - 测试发送验证码API - - 测试验证码登录/注册 - - 测试密码重置 - - 测试频率限制 - -2. Android端测试: - - 测试注册流程(已完成UI) - - 测试登录流程(待完成UI) - - 测试密码重置流程(待完成UI) - - 测试账户管理功能(待完成UI) - -## 🎯 核心功能已就绪 - -后端所有API已经实现并可以使用,Android端的核心组件和ViewModel也已准备就绪。剩余工作主要是完成UI页面的开发和集成测试。 - -## 📞 技术支持 - -如有问题,请参考: -- 后端API文档:`api/docs/README.md` -- 前端实施计划:查看项目根目录下的计划文件 diff --git a/docs/SMS_VERIFICATION_IMPLEMENTATION_SUMMARY.md b/docs/SMS_VERIFICATION_IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index 020fac1..0000000 --- a/docs/SMS_VERIFICATION_IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,616 +0,0 @@ -# 短信验证码功能实施总结 - -## 概述 - -本文档总结了短信验证码功能的完整实施情况,包括后端API、Android客户端、测试和部署等所有方面。 - -## 实施日期 - -2024-01-27 - -## 实施内容 - -### 一、后端实施 (FastAPI) - -#### 1. 数据库模型 - -**新增表:`sms_verification_codes`** - -| 字段 | 类型 | 说明 | -|------|------|------| -| id | VARCHAR | 主键ID | -| phone | VARCHAR | 手机号 | -| code | VARCHAR | 6位验证码 | -| purpose | VARCHAR | 用途(register/login/reset_password/change_phone) | -| is_used | BOOLEAN | 是否已使用 | -| is_expired | BOOLEAN | 是否已过期 | -| expires_at | TIMESTAMP | 过期时间(5分钟) | -| created_at | TIMESTAMP | 创建时间 | -| verified_at | TIMESTAMP | 验证时间 | -| ip_address | VARCHAR | 请求IP地址 | - -**扩展表:`refresh_tokens`** -- 新增字段:`device_info` (VARCHAR) - 设备信息,用于全设备登出 - -**索引:** -- `idx_sms_phone` - 手机号索引 -- `idx_sms_created_at` - 创建时间索引 -- `idx_sms_purpose` - 用途索引 -- `idx_sms_phone_purpose` - 复合索引 - -#### 2. 短信服务 (`api/services/sms_service.py`) - -**核心功能:** -- ✅ 发送验证码(集成腾讯云SMS SDK) -- ✅ 验证验证码 -- ✅ 频率限制(60秒内同一手机号只能发送一次) -- ✅ 验证码过期检查(5分钟) -- ✅ 验证码使用后标记 - -#### 3. 认证路由扩展 (`api/routers/auth.py`) - -**新增API端点:** - -| 端点 | 方法 | 说明 | -|------|------|------| -| `/api/auth/sms/send` | POST | 发送验证码 | -| `/api/auth/login/sms` | POST | 验证码登录 | -| `/api/auth/register/sms` | POST | 验证码注册 | -| `/api/auth/password/reset` | POST | 重置密码 | -| `/api/auth/password/change` | POST | 修改密码(已登录) | -| `/api/auth/phone/change` | POST | 修改手机号 | -| `/api/auth/logout/all` | POST | 登出所有设备 | - -#### 4. 依赖配置 - -**新增依赖:** -``` -tencentcloud-sdk-python>=3.0.1000 -``` - -**环境变量:** -```bash -TENCENT_SMS_SECRET_ID=your_secret_id -TENCENT_SMS_SECRET_KEY=your_secret_key -TENCENT_SMS_SDK_APP_ID=your_app_id -TENCENT_SMS_SIGN_NAME=your_sign_name -# 统一使用一个短信模板ID(所有场景共用) -TENCENT_SMS_TEMPLATE_ID=your_template_id -``` - -### 二、Android端实施 - -#### 1. 网络层扩展 (`AuthService.kt`) - -**新增方法:** -- ✅ `sendVerificationCode()` - 发送验证码 -- ✅ `loginWithSms()` - 验证码登录 -- ✅ `registerWithSms()` - 验证码注册 -- ✅ `resetPassword()` - 重置密码 -- ✅ `changePassword()` - 修改密码 -- ✅ `changePhone()` - 修改手机号 -- ✅ `logoutAll()` - 登出所有设备 - -#### 2. ViewModel扩展 (`AuthViewModel.kt`) - -**新增状态:** -- ✅ `smsCountdown` - 验证码倒计时(60秒) - -**新增方法:** -- ✅ `sendVerificationCode()` - 发送验证码 -- ✅ `loginWithSms()` - 验证码登录 -- ✅ `registerWithSms()` - 验证码注册 -- ✅ `resetPassword()` - 重置密码 -- ✅ `changePassword()` - 修改密码 -- ✅ `changePhone()` - 修改手机号 -- ✅ `logoutAll()` - 登出所有设备 -- ✅ `startCountdown()` - 启动倒计时 - -#### 3. UI组件 - -**可复用组件:** -- ✅ `SmsCodeInput.kt` - 验证码输入框(6位数字,自动聚焦) -- ✅ `SendSmsButton.kt` - 发送验证码按钮(带倒计时) -- ✅ `CompactSendSmsButton.kt` - 紧凑版发送按钮 -- ✅ `PasswordStrengthIndicator.kt` - 密码强度指示器 -- ✅ `PasswordRequirements.kt` - 密码要求提示 - -#### 4. 新增页面 - -**密码重置页面 (`ResetPasswordScreen.kt`)** -- ✅ 手机号输入 -- ✅ 发送验证码(带倒计时) -- ✅ 验证码输入 -- ✅ 新密码输入 -- ✅ 确认密码输入 -- ✅ 密码强度指示 -- ✅ 表单验证 -- ✅ 成功后跳转登录 - -**账户管理页面 (`AccountManagementScreen.kt`)** -- ✅ 账户信息展示 -- ✅ 修改密码对话框 -- ✅ 修改手机号对话框 -- ✅ 登出当前设备 -- ✅ 登出所有设备(带二次确认) - -#### 5. 页面修改 - -**注册页面 (`RegisterScreen.kt`)** -- ✅ 已在之前的实施中完成 -- ✅ 手机号输入框后添加"发送验证码"按钮 -- ✅ 验证码输入框 -- ✅ 倒计时显示 - -**登录页面 (`LoginScreen.kt`)** -- ✅ 添加"忘记密码"链接 -- ✅ 跳转到密码重置页面 - -**个人页面 (`ProfileScreen.kt`)** -- ✅ 移除原有的"登出"按钮 -- ✅ 在"设置"区域添加"账户管理"入口 -- ✅ 仅在已登录时显示 - -#### 6. 导航配置 (`AppNavigation.kt`) - -**新增路由:** -- ✅ `Screen.ResetPassword` - 密码重置页面 -- ✅ `Screen.AccountManagement` - 账户管理页面 - -**路由配置:** -- ✅ 密码重置成功后跳转到登录页面 -- ✅ 登出成功后清空导航栈并跳转到登录页面 -- ✅ 登录页面添加忘记密码回调 - -#### 7. 图标扩展 (`AppIcons.kt`) - -**新增图标:** -- ✅ `ManageAccounts` - 账户管理 -- ✅ `Lock` - 锁(密码) -- ✅ `Phone` - 手机 -- ✅ `ExitToApp` - 登出 -- ✅ `DevicesOther` - 多设备 - -### 三、测试 - -#### 1. 后端测试脚本 (`api/test_sms_verification.py`) - -**测试模式:** -- ✅ 交互式测试(需要真实短信验证码) -- ✅ 自动化测试(需要测试验证码支持) -- ✅ API连接测试 - -**测试用例:** -- ✅ 发送验证码 -- ✅ 频率限制 -- ✅ 验证码注册 -- ✅ 验证码登录 -- ✅ 密码重置 -- ✅ 修改密码 -- ✅ 修改手机号 -- ✅ 登出所有设备 - -#### 2. 测试文档 - -**文档:** -- ✅ `api/docs/短信验证码测试指南.md` - 完整的测试指南 -- ✅ 包含手动测试步骤 -- ✅ 包含常见问题解答 -- ✅ 包含测试检查清单 - -### 四、部署 - -#### 1. 数据库迁移 - -**迁移脚本:** `api/migrations/add_sms_verification.sql` -- ✅ 创建 `sms_verification_codes` 表 -- ✅ 创建索引 -- ✅ 扩展 `refresh_tokens` 表 -- ✅ 添加表和字段注释 - -#### 2. 部署文档 - -**文档:** `api/docs/部署指南.md` -- ✅ 腾讯云短信服务配置步骤 -- ✅ 环境变量配置说明 -- ✅ 数据库迁移步骤 -- ✅ Docker部署配置 -- ✅ Systemd部署配置 -- ✅ GitHub Actions配置 -- ✅ 监控与告警配置 -- ✅ 安全加固建议 -- ✅ 备份与恢复步骤 -- ✅ 故障排查指南 -- ✅ 性能优化建议 - -#### 3. 部署脚本 - -**脚本:** `api/deploy.sh` -- ✅ 自动检查前置条件 -- ✅ 验证环境变量 -- ✅ 安装依赖 -- ✅ 执行数据库迁移 -- ✅ 验证部署 -- ✅ 测试服务连接 - -#### 4. GitHub Actions配置文档 - -**文档:** `github-actions-secrets.md` -- ✅ 列出所有需要配置的Secrets -- ✅ 配置步骤说明 - -## 功能特性 - -### 安全性 - -- ✅ 验证码采用6位随机数字 -- ✅ 5分钟过期时间 -- ✅ 60秒发送频率限制 -- ✅ 验证码使用后立即标记 -- ✅ 密码修改需验证旧密码 -- ✅ 手机号修改需验证码验证 -- ✅ 敏感操作记录IP地址 -- ✅ 密码强度验证 - -### 用户体验 - -- ✅ 倒计时显示(60秒) -- ✅ 自动聚焦验证码输入框 -- ✅ 实时表单验证 -- ✅ 友好的错误提示 -- ✅ 加载状态指示 -- ✅ 成功后自动跳转 -- ✅ 密码强度可视化 -- ✅ 二次确认(危险操作) - -### 性能优化 - -- ✅ 验证码查询使用索引 -- ✅ 数据库连接池 -- ✅ 异步操作 -- ✅ 错误重试机制 - -## 文件清单 - -### 后端文件 - -``` -api/ -├── services/ -│ └── sms_service.py # 短信服务(新增) -├── migrations/ -│ └── add_sms_verification.sql # 数据库迁移脚本(新增) -├── docs/ -│ ├── 短信验证码测试指南.md # 测试指南(新增) -│ └── 部署指南.md # 部署指南(新增) -├── test_sms_verification.py # 测试脚本(新增) -├── deploy.sh # 部署脚本(新增) -├── requirements.txt # 更新依赖 -├── .env.production # 更新环境变量 -└── routers/auth.py # 扩展认证路由 -``` - -### Android文件 - -``` -app-android/app/src/main/java/com/huaga/life_echo/ -├── ui/ -│ ├── screens/ -│ │ ├── ResetPasswordScreen.kt # 密码重置页面(新增) -│ │ ├── AccountManagementScreen.kt # 账户管理页面(新增) -│ │ ├── RegisterScreen.kt # 修改(已在之前完成) -│ │ ├── LoginScreen.kt # 修改 -│ │ └── ProfileScreen.kt # 修改 -│ ├── components/auth/ -│ │ ├── SmsCodeInput.kt # 验证码输入框(已存在) -│ │ ├── SendSmsButton.kt # 发送验证码按钮(已存在) -│ │ └── PasswordStrengthIndicator.kt # 密码强度指示器(已存在) -│ └── icons/ -│ └── AppIcons.kt # 扩展图标 -├── viewmodel/ -│ └── AuthViewModel.kt # 扩展(已在之前完成) -├── network/ -│ ├── AuthService.kt # 扩展(已在之前完成) -│ └── models/ -│ └── AuthModels.kt # 扩展(已在之前完成) -└── navigation/ - └── AppNavigation.kt # 扩展 -``` - -### 文档文件 - -``` -├── SMS_VERIFICATION_IMPLEMENTATION_STATUS.md # 实施状态(已存在) -├── SMS_VERIFICATION_IMPLEMENTATION_SUMMARY.md # 实施总结(本文档) -└── github-actions-secrets.md # GitHub Actions配置(已存在) -``` - -## API文档 - -### 发送验证码 - -```http -POST /api/auth/sms/send -Content-Type: application/json - -{ - "phone": "13800138000", - "purpose": "register" // register/login/reset_password/change_phone -} - -Response 200: -{ - "message": "验证码已发送", - "expires_in": 300 -} - -Response 429: -{ - "detail": "发送过于频繁,请60秒后再试" -} -``` - -### 验证码登录 - -```http -POST /api/auth/login/sms -Content-Type: application/json - -{ - "phone": "13800138000", - "code": "123456" -} - -Response 200: -{ - "access_token": "eyJ...", - "refresh_token": "eyJ...", - "token_type": "bearer" -} -``` - -### 验证码注册 - -```http -POST /api/auth/register/sms -Content-Type: application/json - -{ - "phone": "13800138000", - "code": "123456", - "password": "password123", - "nickname": "用户昵称", - "email": "optional@email.com" -} - -Response 201: -{ - "access_token": "eyJ...", - "refresh_token": "eyJ...", - "token_type": "bearer" -} -``` - -### 重置密码 - -```http -POST /api/auth/password/reset -Content-Type: application/json - -{ - "phone": "13800138000", - "code": "123456", - "new_password": "newpassword123" -} - -Response 200: -{ - "message": "密码重置成功" -} -``` - -### 修改密码(已登录) - -```http -POST /api/auth/password/change -Authorization: Bearer -Content-Type: application/json - -{ - "old_password": "oldpassword123", - "new_password": "newpassword123" -} - -Response 200: -{ - "message": "密码修改成功" -} -``` - -### 修改手机号 - -```http -POST /api/auth/phone/change -Authorization: Bearer -Content-Type: application/json - -{ - "new_phone": "13900139000", - "code": "123456" -} - -Response 200: -{ - "id": "user_id", - "phone": "13900139000", - "nickname": "用户昵称", - ... -} -``` - -### 登出所有设备 - -```http -POST /api/auth/logout/all -Authorization: Bearer - -Response 200: -{ - "message": "已登出所有设备" -} -``` - -## 使用说明 - -### 开发环境测试 - -1. 启动后端服务: -```bash -cd api -python main.py -``` - -2. 运行测试脚本: -```bash -cd api -python test_sms_verification.py -``` - -3. 运行Android应用并测试各个功能 - -### 生产环境部署 - -1. 配置腾讯云短信服务(参考部署指南) - -2. 配置环境变量: -```bash -cd api -cp .env.production.example .env.production -# 编辑 .env.production,填入实际配置 -``` - -3. 运行部署脚本: -```bash -cd api -./deploy.sh -``` - -4. 或使用Docker部署: -```bash -cd api -docker-compose up -d -``` - -## 注意事项 - -### 安全 - -1. **保护API密钥** - - 不要将腾讯云API密钥提交到Git仓库 - - 使用环境变量或密钥管理服务 - - 定期轮换密钥 - -2. **验证码安全** - - 验证码仅5分钟有效 - - 使用后立即失效 - - 60秒频率限制防止暴力破解 - -3. **密码安全** - - 强制最小长度(6位) - - 建议包含大小写字母、数字和特殊字符 - - 密码加密存储 - -### 性能 - -1. **数据库优化** - - 使用索引加速查询 - - 定期清理过期验证码 - - 监控数据库性能 - -2. **短信发送** - - 异步发送避免阻塞 - - 监控发送成功率 - - 设置合理的超时时间 - -3. **缓存** - - 使用Redis缓存频率限制状态 - - 缓存用户会话信息 - -### 监控 - -1. **关键指标** - - 短信发送成功率 - - 验证码验证成功率 - - API响应时间 - - 错误率 - -2. **日志** - - 记录所有短信发送操作 - - 记录验证失败原因 - - 记录异常情况 - -3. **告警** - - 发送失败率过高 - - API响应时间过长 - - 数据库连接失败 - -## 后续优化建议 - -### 功能增强 - -1. **验证码类型** - - 支持语音验证码 - - 支持邮箱验证码 - - 支持图形验证码(防机器人) - -2. **安全增强** - - 设备指纹识别 - - 异常登录检测 - - 多因素认证(MFA) - -3. **用户体验** - - 验证码自动填充(Android SMS Retriever API) - - 记住设备(免验证码登录) - - 生物识别登录 - -### 性能优化 - -1. **缓存优化** - - 缓存用户信息 - - 缓存验证码状态 - - 使用CDN加速 - -2. **数据库优化** - - 分表分库 - - 读写分离 - - 使用时序数据库存储验证码记录 - -3. **服务优化** - - 使用消息队列处理短信发送 - - 负载均衡 - - 服务降级和熔断 - -## 相关文档 - -- [实施状态文档](SMS_VERIFICATION_IMPLEMENTATION_STATUS.md) -- [测试指南](api/docs/短信验证码测试指南.md) -- [部署指南](api/docs/部署指南.md) -- [GitHub Actions配置](github-actions-secrets.md) - -## 联系方式 - -如有问题或建议,请联系开发团队。 - -## 更新日志 - -- 2024-01-27: 完成短信验证码功能实施 - - 后端API实现 - - Android客户端实现 - - 测试脚本和文档 - - 部署脚本和文档 - ---- - -**实施完成日期:** 2024-01-27 -**实施人员:** AI Assistant -**状态:** ✅ 已完成 diff --git a/docs/Skills.md b/docs/Skills.md deleted file mode 100644 index f325bea..0000000 --- a/docs/Skills.md +++ /dev/null @@ -1,17 +0,0 @@ -# 项目通用技术设计(Skills) - -本文档已拆分为独立 Skill 文件,统一放在 **`skills/`** 目录下。 - -👉 **请查看 [skills/README.md](skills/README.md)** 获取完整索引与各块设计说明。 - -## 快速索引 - -| 设计块 | 文件 | -|--------|------| -| 登录与注册机制 | [skills/auth-login-register.md](skills/auth-login-register.md) | -| 顶部导航栏(App Bar) | [skills/top-app-bar.md](skills/top-app-bar.md) | -| 系统状态栏与导航栏 | [skills/system-bars.md](skills/system-bars.md) | -| 底部导航栏(Tab 栏) | [skills/bottom-navigation.md](skills/bottom-navigation.md) | -| Android 路由与导航 | [skills/android-navigation-routing.md](skills/android-navigation-routing.md) | -| 错误处理 | [skills/error-handling.md](skills/error-handling.md) | -| API 与后端约定 | [skills/api-backend-conventions.md](skills/api-backend-conventions.md) | diff --git a/docs/app-expo-deploy.md b/docs/app-expo-deploy.md deleted file mode 100644 index a2e0ff8..0000000 --- a/docs/app-expo-deploy.md +++ /dev/null @@ -1,106 +0,0 @@ -# App Expo 部署文档 - -> app-expo 前端应用的 CI/CD 部署说明,基于 GitHub Actions,无 EAS 依赖。 - -## 概述 - -统一部署 Pipeline 支持三个环境: - -| 环境 | 触发源 | 用途 | -|------|--------|------| -| **dev** | `main` 分支 | 开发 + 内部测试 | -| **stage** | `staging` 分支 | 预发布 | -| **prod** | `v*.*.*` 标签 | 正式发布 | - -## 环境映射 - -环境由触发源自动推断,无需额外配置: - -``` -main → dev -staging → stage -v*.*.* → prod -``` - -## 触发条件 - -- **Push**:推送至 `main` / `staging` 或 `v*.*.*` 标签 -- **路径过滤**:仅当 `app-expo/` 或 `.github/workflows/app-expo-deploy.yml` 有变更时触发 -- **手动触发**:`workflow_dispatch` 可选择 dev / stage / prod - -## 使用方式 - -### Dev(开发 + 内部测试) - -```bash -git push origin main -``` - -合并后自动执行:质量检查、构建、上传 artifact(保留 14 天)。 - -### Stage(预发布) - -```bash -git push origin staging -``` - -若没有 `staging` 分支,可先创建: - -```bash -git checkout -b staging -git push -u origin staging -``` - -构建产物保留 30 天。 - -### Prod(正式发布) - -```bash -git tag v1.0.0 -git push origin v1.0.0 -``` - -将自动:构建、打包 zip、创建 GitHub Release、上传 artifact(保留 90 天)。 - -### 手动触发 - -1. 打开仓库 **Actions** 标签 -2. 选择 **App Expo Deploy** -3. 点击 **Run workflow** -4. 选择环境(dev / stage / prod) -5. 若选 prod,可填写版本号(如 `1.0.0`) - -## 版本规则(SemVer) - -| 类型 | 示例 | 说明 | -|------|------|------| -| **PATCH** | v1.0.0 → v1.0.1 | bug 修复、小改动 | -| **MINOR** | v1.0.0 → v1.1.0 | 新功能、向后兼容 | -| **MAJOR** | v1.0.0 → v2.0.0 | 重大变更、不兼容 | - -## 各环境行为 - -| 步骤 | dev | stage | prod | -|------|-----|-------|------| -| 质量检查(format/lint/test) | ✓ | - | - | -| Web 构建 | ✓ | ✓ | ✓ | -| 上传 artifact | ✓ (14 天) | ✓ (30 天) | ✓ (90 天) | -| 创建 GitHub Release | - | - | ✓ | - -## GitHub Environments - -在 **Settings → Environments** 中可配置 `dev`、`staging`、`production`: - -- 为各环境配置独立 secrets -- 为 production 配置审批规则 -- 环境与 workflow 自动关联 - -## 产物说明 - -- **dev / stage**:`app-expo/dist` 目录(静态 Web 构建) -- **prod**:`app-expo-v{version}-web.zip`,附带于 GitHub Release - -## 相关文件 - -- 工作流:`.github/workflows/app-expo-deploy.yml` -- 应用配置:`app-expo/app.config.ts` diff --git a/docs/doc.md b/docs/doc.md deleted file mode 100644 index fcc8122..0000000 --- a/docs/doc.md +++ /dev/null @@ -1,414 +0,0 @@ -# 《岁月时书》MVP – User Stories(V1) - -> **视角说明** -> -> * 用户:普通用户(偏中老年,但 MVP 不特化) -> * AI:回忆录助手 -> * 客户端:iOS / Android(React Native / Flutter / 原生均适用) -> * 后端:API + AI 服务 -> * 数据库:关系型 or 文档型均可(这里用逻辑结构描述) - ---- - -## EPIC 0:基础应用框架 - -### US-0.1 应用启动与默认入口 - -**User Story** - -> 作为用户 -> 我希望打开 App 后直接看到「对话列表」 -> 这样我不用学习就知道从哪里开始 - -**前端(iOS / Android)** - -* App 启动后默认进入 `chat-list` -* 显示 App 名称 + 副标题 -* 底部导航栏可见 - -**后端** - -* 可无登录状态返回「默认对话列表」(MVP 允许匿名) - -**数据库** - -* `conversation`(至少一条系统内置对话) - - * id - * type = `system` - * title = `回忆录助手` - * last_message - * updated_at - ---- - -## EPIC 1:对话列表(chat-list) - -### US-1.1 查看对话列表 - -**User Story** - -> 作为用户 -> 我希望看到一个类似微信的联系人列表 -> 能快速进入和 AI 的聊天 - -**前端** - -* 列表样式(头像 + 名称 + 最近消息 + 时间) -* 默认只有一个联系人:`回忆录助手` -* 点击进入聊天详情 - -**后端** - -* 返回用户的 conversation 列表 -* 按 `updated_at` 排序 - -**数据库** - -* `conversation` - - * id - * user_id(可为空,MVP) - * title - * avatar_type(AI / 用户) - * last_message - * updated_at - ---- - -### US-1.2 查看新消息预览 - -**User Story** - -> 作为用户 -> 我希望在对话列表中看到最近一条消息 -> 让我知道“书有没有在变厚” - -**前端** - -* 单行消息预览(省略) -* 时间戳显示(今天 / 昨天 / 日期) - -**后端** - -* 返回 conversation 的 last_message -* 在新消息生成后更新 - -**数据库** - -* `conversation.last_message` -* `conversation.updated_at` - ---- - -## EPIC 2:聊天详情(chat) - -### US-2.1 进入全屏聊天 - -**User Story** - -> 作为用户 -> 我希望进入一个沉浸式聊天界面 -> 就像在微信里和一个人聊天一样 - -**前端** - -* 顶部栏:返回 + 回忆录助手 + 在线状态 -* 无底部导航 -* 消息列表支持滚动 - -**后端** - -* 返回该 conversation 的消息列表(分页) - -**数据库** - -* `message` - - * id - * conversation_id - * sender_type(user / ai) - * content_type(text / image / audio) - * content - * created_at - ---- - -### US-2.2 发送文字消息 - -**User Story** - -> 作为用户 -> 我希望能输入文字并发送 -> 来讲述我的回忆 - -**前端** - -* 文本输入框 -* 有文字时显示发送按钮 -* 发送后立即显示在聊天区(optimistic) - -**后端** - -* 接收用户消息 -* 保存消息 -* 触发 AI 回复流程 - -**数据库** - -* `message` - - * sender_type = `user` - * content_type = `text` - * content = 原始文本 - ---- - -### US-2.3 AI 自动回复并引导 - -**User Story** - -> 作为用户 -> 我希望 AI 能主动提问和追问 -> 帮我把记忆说完整 - -**前端** - -* 显示“正在输入…” -* AI 回复显示为左侧气泡 - -**后端** - -* 将上下文消息发送给 AI -* 返回 AI 回复 -* 回复内容偏引导式 - -**数据库** - -* `message` - - * sender_type = `ai` - * content_type = `text` - * metadata(可选:tag = 引导 / 追问) - ---- - -### US-2.4 发送语音消息(占位) - -**User Story** - -> 作为用户 -> 我希望能按住说话 -> 像微信一样讲故事 - -**前端** - -* 按住说话 / 上滑取消 -* 本地生成音频文件 -* 显示语音气泡(时长) - -**后端(MVP 可弱化)** - -* 接收音频文件 -* 返回占位文本(或不转写) - -**数据库** - -* `message` - - * content_type = `audio` - * content = 音频文件 URL - * duration - ---- - -### US-2.5 发送图片 - -**User Story** - -> 作为用户 -> 我希望能发老照片 -> 帮助 AI 更好理解我的故事 - -**前端** - -* 从相册选择 / 拍照 -* 图片气泡展示 - -**后端** - -* 接收图片 -* 存储并返回 URL - -**数据库** - -* `message` - - * content_type = `image` - * content = 图片 URL - * metadata(width / height) - ---- - -## EPIC 3:回忆录生成与阅读 - -### US-3.1 自动生成回忆录结构 - -**User Story** - -> 作为用户 -> 我希望聊天内容能自动变成“书” -> 而不是只是一堆对话 - -**前端** - -* 回忆录 Tab 显示目录页 -* 即使内容不多,也能看到章节雏形 - -**后端** - -* 定期 / 实时整理对话 -* 生成章节结构 - -**数据库** - -* `memoir` - - * id - * user_id - * title - * subtitle - * updated_at - -* `memoir_chapter` - - * id - * memoir_id - * title - * order - * content - * progress - ---- - -### US-3.2 阅读章节内容 - -**User Story** - -> 作为用户 -> 我希望能像读电子书一样 -> 阅读已经整理好的回忆 - -**前端** - -* 章节标题 + 正文 -* 舒适排版 -* 插图内嵌展示 - -**后端** - -* 返回章节内容(HTML / Markdown) - -**数据库** - -* `memoir_chapter.content` -* `memoir_asset`(插图) - - * id - * chapter_id - * type - * url - ---- - -### US-3.3 显示“最近更新” - -**User Story** - -> 作为用户 -> 我希望知道最近哪一章有新增内容 -> 感觉“书在变厚” - -**前端** - -* 目录页显示「最近更新」 -* 章节进度条 - -**后端** - -* 计算章节更新时间 - -**数据库** - -* `memoir_chapter.updated_at` - ---- - -## EPIC 4:我的(个人页) - -### US-4.1 查看账户状态 - -**User Story** - -> 作为用户 -> 我希望在一个地方看到我的账户和套餐 -> 不影响聊天体验 - -**前端** - -* 头像 + 昵称 -* 当前套餐(免费) - -**后端** - -* 返回用户信息 - -**数据库** - -* `user` - - * id - * nickname - * avatar - * plan_type - ---- - -### US-4.2 数据导出入口(占位) - -**User Story** - -> 作为用户 -> 我希望知道我的数据是可以导出的 -> 哪怕现在还不能用 - -**前端** - -* 导出按钮(灰态) -* 提示说明 - -**后端** - -* 返回状态:未开放 - -**数据库** - -* 无新增(MVP) - ---- - -## MVP User Stories 总览(Checklist) - -**核心路径** - -* ☑ 对话列表 -* ☑ 聊天(文字 + AI 引导) -* ☑ 回忆录自动生成 -* ☑ 电子书阅读体验 - -**增强体验** - -* ☑ 图片 -* ⏳ 语音 -* ⏳ PDF 导出 -* ⏳ 登录 / 套餐 diff --git a/docs/image-1.png b/docs/image-1.png deleted file mode 100644 index 78dccc3..0000000 Binary files a/docs/image-1.png and /dev/null differ diff --git a/docs/image-2.png b/docs/image-2.png deleted file mode 100644 index e39fbef..0000000 Binary files a/docs/image-2.png and /dev/null differ diff --git a/docs/image-4.png b/docs/image-4.png deleted file mode 100644 index 2a4b937..0000000 Binary files a/docs/image-4.png and /dev/null differ diff --git a/docs/image.png b/docs/image.png deleted file mode 100644 index 7020221..0000000 Binary files a/docs/image.png and /dev/null differ diff --git a/docs/memoir-image-status-contract.md b/docs/memoir-image-status-contract.md deleted file mode 100644 index 222a1bd..0000000 --- a/docs/memoir-image-status-contract.md +++ /dev/null @@ -1,45 +0,0 @@ -# 回忆录图片状态契约 - -本文档描述章节图片资产在接口层的字段语义,供后端、Android 客户端和后续其他前端实现统一使用。 - -## 字段说明 - -- `status` - - `pending`: 等待生成 - - `processing`: 正在生成 - - `completed`: 生成完成 - - `failed`: 生成失败 -- `url` - - 仅当图片当前可直接展示时才应为非空 - - 私有 COS 桶签名失败时,必须置空,不能回退为未签名的原始 COS URL -- `retryable` - - `null`: 当前不是失败态,或该字段不适用 - - `true`: 失败但仍可重试 - - `false`: 失败且不可重试,属于永久失败 -- `error` - - 面向调试和展示的错误信息 - -## 推荐组合语义 - -| status | url | retryable | 语义 | -| --- | --- | --- | --- | -| `pending` | `null` | `null` | 任务尚未开始或等待处理 | -| `processing` | `null` | `null` | 图片正在生成 | -| `completed` | 非空 | `null` | 图片可直接展示 | -| `completed` | `null` | `null` | 图片已完成但当前不可投递,例如私有桶签名失败 | -| `failed` | `null` | `true` | 本次失败可重试 | -| `failed` | `null` | `false` | 永久失败,应明确展示终态 | - -## 客户端处理建议 - -- `pending` / `processing`: 显示“图片生成中” -- `completed` 且 `url` 非空: 正常展示图片 -- `completed` 且 `url` 为空: 显示“图片暂不可用” -- `failed` 且 `retryable=true`: 显示“图片生成失败,稍后重试” -- `failed` 且 `retryable=false`: 显示“图片生成失败,暂不可恢复” - -## 约束 - -- 后端在规范化图片资产时必须保留 `retryable` -- 非失败态不应输出 `retryable=true/false` -- 客户端轮询是否继续,仍以工作态 `pending/processing` 为准,不因 `failed + retryable=true` 自动延长轮询窗口 diff --git a/docs/network-config-guide.md b/docs/network-config-guide.md deleted file mode 100644 index 4736b09..0000000 --- a/docs/network-config-guide.md +++ /dev/null @@ -1,138 +0,0 @@ -# 网络配置指南 - -## 配置说明 - -### 当前配置 - -1. **Nginx 配置**(已连接到外部网络): -```yaml -networks: - life-echo-network: - external: true - name: api_life-echo-network -``` - -2. **Life-Echo API 配置**(已更新为使用相同外部网络): -```yaml -networks: - life-echo-network: - external: true - name: api_life-echo-network -``` - -3. **Nginx upstream 配置**(使用服务名): -```nginx -upstream lifecho_api_backend { - server api:8000; -} -``` - -## 部署步骤 - -### 1. 确保网络存在 - -首先检查网络是否存在: - -```bash -docker network ls | grep api_life-echo-network -``` - -如果网络不存在,需要先创建: - -```bash -docker network create api_life-echo-network -``` - -### 2. 启动 Life-Echo 服务 - -```bash -cd /home/ubuntu/production/lifecho/api -docker-compose down -docker-compose up -d -``` - -### 3. 验证网络连接 - -检查容器是否在正确的网络中: - -```bash -# 检查网络中的容器 -docker network inspect api_life-echo-network - -# 应该能看到: -# - nginx 容器 -# - life-echo-api-prod 容器 -# - life-echo-postgres 容器 -# - life-echo-redis 容器 -# - life-echo-celery-worker 容器 -``` - -### 4. 测试连接 - -从 nginx 容器内测试: - -```bash -# 进入 nginx 容器 -docker exec -it nginx sh - -# 测试 DNS 解析 -nslookup api -# 或 -ping api - -# 测试 HTTP 连接 -wget -O- http://api:8000/health -``` - -### 5. 重启 Nginx - -```bash -docker restart nginx -``` - -## 故障排查 - -### 问题 1:网络名称不匹配 - -如果遇到网络名称错误,检查实际网络名称: - -```bash -# 查看所有网络 -docker network ls - -# 查看 life-echo 服务使用的网络 -docker inspect life-echo-api-prod | grep -A 10 Networks -``` - -然后更新 docker-compose.yml 中的网络名称。 - -### 问题 2:服务名无法解析 - -如果 `api:8000` 不工作,可以尝试使用容器名: - -```nginx -upstream lifecho_api_backend { - server life-echo-api-prod:8000; -} -``` - -### 问题 3:网络连接失败 - -如果容器无法连接到网络,手动连接: - -```bash -# 连接现有容器到网络 -docker network connect api_life-echo-network life-echo-api-prod -docker network connect api_life-echo-network life-echo-postgres -docker network connect api_life-echo-network life-echo-redis -docker network connect api_life-echo-network life-echo-celery-worker -``` - -## 验证清单 - -- [ ] 网络 `api_life-echo-network` 存在 -- [ ] Life-Echo 服务使用外部网络配置 -- [ ] Nginx 配置使用服务名 `api:8000` -- [ ] 所有容器都在同一网络中 -- [ ] Nginx 可以解析 `api` 主机名 -- [ ] HTTP 健康检查通过:`curl https://lifecho.worldsplats.com/health` diff --git a/docs/nginx-config-guide.md b/docs/nginx-config-guide.md deleted file mode 100644 index ba7b72d..0000000 --- a/docs/nginx-config-guide.md +++ /dev/null @@ -1,109 +0,0 @@ -# Nginx 配置调整指南 - -## 问题说明 - -根据 `docker-compose.yml` 配置: -- **服务名**:`api` -- **容器名**:`life-echo-api-prod` -- **网络名**:`life-echo-network` -- **端口映射**:`8000:8000` - -原配置中使用的是 `lifecho-api:8000`,需要根据实际部署方式调整。 - -## 配置方案 - -### 方案 1:Nginx 在同一个 Docker Compose 网络中(推荐) - -如果 nginx 容器也在 `life-echo-network` 网络中,使用**服务名**: - -```nginx -upstream lifecho_api_backend { - server api:8000; -} -``` - -**如何实现**: -1. 在 nginx 的 docker-compose 配置中添加网络: -```yaml -services: - nginx: - # ... 其他配置 - networks: - - life-echo-network - -networks: - life-echo-network: - external: true # 使用已存在的网络 -``` - -### 方案 2:Nginx 在外部容器但连接到同一网络 - -如果 nginx 容器连接到 `life-echo-network` 网络,可以使用**容器名**: - -```nginx -upstream lifecho_api_backend { - server life-echo-api-prod:8000; -} -``` - -**如何实现**: -```bash -# 将 nginx 容器连接到网络 -docker network connect life-echo-network -``` - -### 方案 3:Nginx 在主机上(通过端口映射) - -如果 nginx 直接运行在主机上,使用**本地端口**: - -```nginx -upstream lifecho_api_backend { - server 127.0.0.1:8000; -} -``` - -这是最简单的方案,因为 `docker-compose.yml` 已经将容器的 8000 端口映射到主机的 8000 端口。 - -## 推荐配置 - -根据最常见的部署场景,**推荐使用方案 1(服务名)**,因为: -1. 更灵活,不依赖容器名 -2. Docker Compose 会自动处理服务发现 -3. 如果容器重启,服务名保持不变 - -## 验证配置 - -配置完成后,测试连接: - -```bash -# 测试健康检查端点 -curl https://lifecho.worldsplats.com/health - -# 测试 API 端点 -curl https://lifecho.worldsplats.com/api/your-endpoint -``` - -## 故障排查 - -如果连接失败,检查: - -1. **网络连接**: -```bash -# 检查容器是否在同一网络 -docker network inspect life-echo-network - -# 检查容器 IP -docker inspect life-echo-api-prod | grep IPAddress -``` - -2. **端口映射**: -```bash -# 检查端口是否映射 -docker ps | grep life-echo-api-prod -``` - -3. **Nginx 日志**: -```bash -# 查看错误日志 -tail -f /var/log/nginx/error.log -``` diff --git a/docs/nginx-fix-guide.md b/docs/nginx-fix-guide.md deleted file mode 100644 index 142a4fc..0000000 --- a/docs/nginx-fix-guide.md +++ /dev/null @@ -1,121 +0,0 @@ -# Nginx 配置修复指南 - -## 错误信息 -``` -host not found in upstream "life-echo-api-prod:8000" -``` - -## 问题原因 -nginx 容器无法解析 `life-echo-api-prod` 主机名,说明: -1. nginx 容器不在 `life-echo-network` 网络中 -2. 或者应该使用服务名而不是容器名 - -## 解决方案 - -### 方案 1:使用主机端口(最简单,推荐) - -如果 nginx 在主机上运行,或通过主机端口访问,使用: - -```nginx -upstream lifecho_api_backend { - server 127.0.0.1:8000; -} -``` - -**优点**:不需要配置网络,因为 `docker-compose.yml` 已经将容器的 8000 端口映射到主机的 8000 端口。 - -### 方案 2:将 nginx 连接到同一网络 - -如果 nginx 在容器中运行,需要将其连接到 `life-echo-network` 网络: - -#### 步骤 1:检查网络是否存在 -```bash -docker network ls | grep life-echo-network -``` - -#### 步骤 2:将 nginx 容器连接到网络 -```bash -# 如果 nginx 容器正在运行 -docker network connect life-echo-network - -# 或者重启 nginx 容器时添加网络 -docker run -d \ - --name nginx \ - --network life-echo-network \ - -v /path/to/nginx.conf:/etc/nginx/nginx.conf \ - nginx:latest -``` - -#### 步骤 3:使用服务名配置 -```nginx -upstream lifecho_api_backend { - server api:8000; # 使用服务名,不是容器名 -} -``` - -### 方案 3:在 docker-compose.yml 中添加 nginx 服务 - -如果希望统一管理,可以在 `docker-compose.yml` 中添加 nginx 服务: - -```yaml -services: - # ... 其他服务 ... - - nginx: - image: nginx:alpine - container_name: life-echo-nginx - ports: - - "80:80" - - "443:443" - volumes: - - ./nginx.conf:/etc/nginx/nginx.conf:ro - - ./ssl:/etc/nginx/ssl:ro - depends_on: - - api - networks: - - life-echo-network - restart: always -``` - -然后使用服务名: -```nginx -upstream lifecho_api_backend { - server api:8000; -} -``` - -## 验证修复 - -1. **检查配置语法**: -```bash -nginx -t -``` - -2. **重启 nginx**: -```bash -# 如果 nginx 在容器中 -docker restart - -# 如果 nginx 在主机上 -sudo systemctl restart nginx -# 或 -sudo nginx -s reload -``` - -3. **测试连接**: -```bash -# 测试健康检查 -curl http://127.0.0.1:8000/health - -# 测试通过 nginx -curl https://lifecho.worldsplats.com/health -``` - -## 当前推荐配置 - -根据错误信息,建议先使用**方案 1**(主机端口),因为: -- 最简单,不需要配置网络 -- `docker-compose.yml` 已经映射了端口 -- 适用于大多数部署场景 - -如果方案 1 不工作(nginx 在容器中且无法访问主机网络),再使用方案 2 或 3。 diff --git a/docs/plans/2026-03-19-image-intent-placeholder-removal-design.md b/docs/plans/2026-03-19-image-intent-placeholder-removal-design.md deleted file mode 100644 index a09c323..0000000 --- a/docs/plans/2026-03-19-image-intent-placeholder-removal-design.md +++ /dev/null @@ -1,345 +0,0 @@ -# Image Intent 化与占位符退役设计 - -> 日期:2026-03-19 -> 前提:`story-first + markdown-first` 总体重构已完成。 -> 目标:彻底移除 `{{IMAGE:描述}}` 这类正文占位符,把图片生成与回填改造成结构化 image intent 流程。 - -## 1. 结论 - -本设计的核心决策如下: - -1. `{{IMAGE:描述}}` 不再是正文协议。 -2. story 正文只保留最终可阅读的 markdown。 -3. 每个 story 必须且仅有一张主插图。 -4. 插图属于 story,封面属于 chapter。 -5. 图片“待生成意图”以结构化数据存储,不再嵌入正文。 -6. 正文中的图片只允许最终引用形式,例如 `![caption](asset://image_id)`。 - -一句话概括新流程: - -`story markdown -> extract image intent -> generate asset -> write new story version with asset:// reference` - -## 2. 问题定义 - -旧占位符方案存在以下问题: - -- 生成意图与正文内容耦合,污染 markdown 真源。 -- 占位符兼容双层、四层、多层花括号,协议不稳定。 -- 图片样式模板被直接拼进占位符字符串,数据边界混乱。 -- 旧链路依赖 `section` 拆分,和 `story-first` 架构冲突。 -- 占位符错误、残留或格式偏差会直接泄漏到阅读层。 -- “每 3 段 1 图”这类旧 fallback 是技术债,不应继续存在。 - -因此,新系统必须彻底移除正文占位符,把图片生成升级成 story/chapter 的结构化资产流程。 - -## 3. 目标与非目标 - -### 3.1 目标 - -- 让图片生成链路与 story/chapter 正文解耦。 -- 保证每个 story 恰好一张主插图。 -- 让 chapter 封面从章节内全部 stories 聚合生成。 -- 让 app、PDF、未来运营端消费统一的 markdown 图片协议。 -- 建立可重试、可审计、可回填的图片版本链。 - -### 3.2 非目标 - -- 不保留 `{{IMAGE:描述}}` 作为线上兼容格式。 -- 不支持 story 多张正文插图的一期能力。 -- 不在本阶段做通用媒体编辑器。 -- 不把封面也写回 chapter 正文 markdown。 - -## 4. 核心模型 - -### 4.1 Story 插图意图 - -建议新增 `story_image_intents`: - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| `id` | string | 主键 | -| `story_id` | string | 所属 story | -| `story_version_id` | string | 提取意图时对应的正文版本 | -| `intent_role` | string | 固定为 `primary` | -| `caption` | string | 最终图注候选 | -| `prompt_brief` | text | 供出图使用的结构化场景摘要 | -| `style_profile` | string/null | 风格策略键 | -| `status` | string | pending / processing / completed / failed | -| `asset_id` | string/null | 生成成功后的资产 | -| `error` | text/null | 错误信息 | -| `created_at` | datetime | 创建时间 | -| `updated_at` | datetime | 更新时间 | - -关键约束: - -- 每个 active story 只能有 1 条 `intent_role=primary` 的有效 intent。 - -### 4.2 Chapter 封面意图 - -建议新增 `chapter_cover_intents`: - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| `id` | string | 主键 | -| `chapter_id` | string | 所属 chapter | -| `chapter_version_id` | string | 封面生成时对应的章节版本 | -| `story_ids` | json | 参与聚合的 stories | -| `prompt_brief` | text | 章节封面摘要 | -| `status` | string | pending / processing / completed / failed | -| `asset_id` | string/null | 封面资产 | -| `error` | text/null | 错误信息 | -| `created_at` | datetime | 创建时间 | -| `updated_at` | datetime | 更新时间 | - -### 4.3 统一资源表 - -建议统一使用 `assets` 或在现有图片表基础上重构: - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| `id` | string | 主键 | -| `asset_type` | string | story_image / chapter_cover | -| `storage_key` | string | 对象存储键 | -| `url` | string/null | 可访问地址 | -| `provider` | string | 生成 provider | -| `style_profile` | string/null | 风格配置 | -| `prompt_final` | text | 最终发送给模型的 prompt | -| `status` | string | completed / failed / deleted | -| `width` | int/null | 宽 | -| `height` | int/null | 高 | -| `created_at` | datetime | 创建时间 | - -## 5. 新的正文协议 - -### 5.1 允许形式 - -正文 markdown 中只允许最终图片引用: - -```md -![奶奶坐在院子里的藤椅上](asset://img_123) -``` - -### 5.2 禁止形式 - -以下形式全部退出线上正文: - -- `{{IMAGE:描述}}` -- `{{{{IMAGE:描述}}}}` -- `` -- 任意 HTML 媒体占位标记 - -### 5.3 解释 - -正文只服务阅读与导出,不再承载“待生成意图”。 - -待生成意图只存在于结构化表中。 - -## 6. 流程设计 - -### 6.1 Story 主插图流程 - -```mermaid -flowchart LR - A["StorySynthesisAgent"] --> B["story canonical markdown"] - B --> C["StoryImageIntentExtractor"] - C --> D["story_image_intents"] - D --> E["ImageGenerationTask"] - E --> F["assets"] - F --> G["Create new story_version"] - G --> H["story markdown includes asset:// reference"] -``` - -步骤: - -1. `StorySynthesisAgent` 生成或更新 story canonical markdown。 -2. `StoryImageIntentExtractor` 从 story markdown 或 AST 中提取唯一主图意图。 -3. 写入 `story_image_intents`,状态为 `pending`。 -4. 异步图片任务读取 intent,生成资产。 -5. 成功后写入 `assets`。 -6. 创建新的 `story_version`,把 markdown 中对应位置回填成 `asset://` 图片引用。 -7. 更新 `stories.current_version_id`。 - -### 6.2 Chapter 封面流程 - -```mermaid -flowchart LR - A["ChapterComposerOrchestrator"] --> B["chapter markdown"] - B --> C["Aggregate chapter stories"] - C --> D["chapter_cover_intent"] - D --> E["Cover image generation"] - E --> F["cover asset"] - F --> G["chapters.cover_asset_id"] -``` - -步骤: - -1. `ChapterComposerOrchestrator` 完成章节编排。 -2. 聚合本章 stories 的人物、地点、时间、情绪、时代背景。 -3. 生成唯一 `chapter_cover_intent`。 -4. 生成封面资源并绑定到 `chapters.cover_asset_id`。 - -说明: - -- 封面不回写进正文 markdown。 -- 阅读页顶部可单独展示封面 asset。 - -## 7. Image Intent 提取策略 - -### 7.1 规则 - -每个 story 必须且仅有一张主插图,因此 extractor 不做多图候选池。 - -优先级: - -1. 最具画面感的场景段落 -2. 具有人物 + 动作 + 场景 + 时代细节的段落 -3. 故事转折点或记忆锚点段落 -4. 若 story 过于抽象,则退化为“人物/地点/时代感”概括图 - -### 7.2 输出 - -输出结构至少包含: - -- `caption` -- `prompt_brief` -- `style_profile` - -### 7.3 失败兜底 - -如果规则和 agent 都未提取到高质量意图,则使用最小兜底策略: - -- story title -- story stage -- time refs -- place refs -- people refs -- story summary - -即使降级,也必须生成 1 条 primary intent。 - -## 8. 版本回填策略 - -### 8.1 原则 - -图片生成成功后,不能原地覆盖 story 正文。 - -必须: - -1. 基于当前 story version 创建新版本 -2. 将最终图片引用回填到 markdown -3. 写入 `change_summary` -4. 更新当前生效版本指针 - -### 8.2 回填位置 - -主插图引用追加在**正文末尾**(`![alt](asset://…)` 的 `alt` 使用 `prompt_brief`)。仍需创建新版本,不可丢图。 - -## 9. 状态机 - -### 9.1 Story 状态建议 - -- `content_pending` -- `content_ready_image_pending` -- `content_ready_image_processing` -- `published` -- `image_failed` - -### 9.2 约束 - -- `published` story 必须有 resolved primary image asset -- `content_ready_image_pending` 允许正文已就绪但图片仍在处理中 -- `image_failed` 允许重试,但不允许伪装成已发布完整内容 - -## 10. 失败处理 - -### 10.1 意图提取失败 - -- 走 deterministic fallback -- 必须产出 intent - -### 10.2 图片生成失败 - -- intent 状态置为 `failed` -- story 状态置为 `content_ready_image_pending` 或 `image_failed` -- 支持后台重试 - -### 10.3 回填失败 - -- asset 保留 -- intent 状态可为 `completed_but_unapplied` -- 创建修复任务重新生成 story version - -### 10.4 Chapter 封面失败 - -- 不影响章节正文阅读 -- 允许章节无封面但正文可读 - -## 11. 测试计划 - -### 11.1 单元测试 - -- 每个 story 只能生成一个 primary intent -- abstract story 走 fallback 也能生成 intent -- 回填后 markdown 只含 `asset://`,不含 placeholder - -### 11.2 集成测试 - -- story markdown -> image intent -> asset -> new story version -- chapter stories -> cover intent -> cover asset - -### 11.3 迁移测试 - -- 旧 `{{IMAGE:描述}}` 正文可被正确提取为 intent -- 旧图片记录可被映射为 asset -- 迁移后正文不再含 placeholder - -### 11.4 渲染测试 - -- app 阅读页正确渲染 `asset://` -- PDF 正确渲染 story 图片与 chapter 封面 -- 未解析外链或非法资源时安全失败 - -## 12. 旧链路退役清单 - -以下逻辑应退出线上主链路: - -- `inject_image_placeholder_template` -- `inject_placeholders` -- `parse_image_placeholders` -- `split_narrative_to_sections` -- `parse_narrative_to_sections` -- 基于 placeholder 创建段落配图的逻辑 -- “每 3 段 1 图”的旧 fallback - -注意: - -- 这些逻辑可短期保留在离线迁移脚本中读取历史数据 -- 但不允许继续出现在线上写路径和读路径 - -## 13. 一次性实施步骤 - -1. 新增 `story_image_intents` -2. 新增 `chapter_cover_intents` -3. 统一资源表为 `assets` -4. 删除 prompt 中对 `{{IMAGE:描述}}` 的输出要求 -5. 重写 story 生成链:正文生成后提取 primary image intent -6. 重写图片任务:读取 intent,不读取正文占位符 -7. 重写 story version 回填逻辑:写入 `asset://` -8. 重写 chapter 封面聚合逻辑 -9. app / PDF 渲染只认 `asset://` -10. 迁移历史正文与旧图片记录 -11. 删除旧占位符相关线上逻辑与测试 -12. 补齐新 intent / asset / cover 测试 - -## 14. 最终判断 - -这次“修复占位符”本质上不是字符串格式修复,而是把旧的正文 DSL 彻底退役。 - -正确的长期模型应当是: - -- story 有且只有一张主插图 -- chapter 有一张聚合封面 -- 图片意图是结构化资产流程 -- markdown 只保存最终可阅读结果 - -只要这四点成立,未来无论是运营润色、重新生成图片、替换封面、审计版本还是导出 PDF,都不需要再碰 `{{IMAGE:描述}}` 这类过渡协议。 diff --git a/docs/plans/2026-03-19-story-first-markdown-first-design.md b/docs/plans/2026-03-19-story-first-markdown-first-design.md deleted file mode 100644 index 21e7404..0000000 --- a/docs/plans/2026-03-19-story-first-markdown-first-design.md +++ /dev/null @@ -1,687 +0,0 @@ -# Story-First + Markdown-First 重构设计 - -> 日期:2026-03-19 -> 目标:一次性重构 Life Echo 的回忆录生成链路,取消 `section` 作为正文真源,建立 `evidence -> story -> chapter -> delivery` 的新架构。 - -## 1. 结论 - -本次重构采用以下不可回退的架构决策: - -1. `story` 是创作真源。 -2. `chapter` 是阅读与导出视图,不再是创作真源。 -3. `markdown` 是唯一正文真源。 -4. `memory/RAG` 是 story 与 chapter 的证据底座,不再只是章节生成的辅助能力。 -5. `agent` 只负责分析、决策、生成,不直接写数据库。 -6. 本次采用一次性切换,不保留旧运行时兼容路径。 - -一句话概括新模型: - -`conversation/segment -> memory evidence -> story markdown -> chapter markdown -> app reading/pdf/export` - -## 2. 目标与非目标 - -### 2.1 目标 - -- 建立 `story-first` 的长期可演进数据模型。 -- 建立 `markdown-first` 的统一正文契约,支持移动端阅读优化。 -- 建立清晰的 multi-agent 编排边界,避免 agent 与持久化耦合。 -- 建立可追溯的 memory/RAG 证据体系,支撑访谈追问与回忆录写作。 -- 为未来独立 Web 运营润色端预留版本管理、审计、回滚能力。 -- 一次性切换到新模型,不留下 `chapter/section` 双真源债务。 - -### 2.2 非目标 - -- 不引入 Braintrust、Vellum 等外部 AI 平台作为当前架构依赖。 -- 不做运行时向后兼容。 -- 不保留 `chapter_sections` 作为正文真源。 -- 不在本阶段开放用户端手工编辑 markdown。 -- 不在本阶段引入复杂富文本、HTML 或通用文档编辑器协议。 - -## 3. 设计原则 - -1. 单一真源:正文只能有一份 canonical markdown。 -2. 可追溯:story 与 chapter 必须能追到 evidence、版本、生成来源。 -3. 单向依赖:`evidence -> story -> chapter -> delivery`,禁止反向污染。 -4. 可替换:LLM runtime、检索策略、渲染实现都应可替换,但内容模型不漂移。 -5. 可验证:所有 AI 生成结果必须有版本和测试护栏。 -6. 可运营:未来运营端进入时,不需要重构底层模型。 - -## 4. 总体架构 - -```mermaid -flowchart LR - A["Conversation / Segments"] --> B["Memory Ingest"] - B --> C["Evidence Layer"] - C --> D["Celery: Route + Narrative"] - D --> E["Story Layer"] - E --> F["Compose from story_links"] - F --> G["Chapter Layer"] - G --> H["App Reading"] - G --> I["PDF Export"] - G --> J["Image Delivery"] -``` - -系统分四层: - -### 4.1 Evidence Layer - -负责原始素材沉淀、检索、结构化事实抽取与时间线组织。 - -核心表: - -- `memory_sources` -- `memory_chunks` -- `memory_facts` -- `timeline_events` -- `memory_summaries` - -职责: - -- 存储 transcript、note、draft 等来源 -- chunk、embedding、FTS、fact extraction、timeline hints -- 为 conversation RAG 和 writing RAG 提供 evidence bundle - -### 4.2 Story Layer - -负责“可独立讲述的一段人生故事”的形成、演进、追溯和版本管理。 - -核心表: - -- `stories` -- `story_versions` -- `story_evidence_links` - -职责: - -- 聚合 evidence 成 story -- 维护 story 的 canonical markdown -- 记录版本链和 AI/编辑来源 -- 作为 chapter 编排的唯一创作输入 - -### 4.3 Chapter Layer - -负责把多个 stories 编排成可阅读、可导出的章节。 - -核心表: - -- `chapters` -- `chapter_versions` -- `chapter_story_links` - -职责: - -- 组织阅读顺序 -- 生成章节级 markdown -- 承担目录、阅读页、导出页的消费语义 - -### 4.4 Delivery Layer - -负责消费 chapter,服务移动端阅读、PDF 导出、图片资源、阅读设置。 - -核心资源: - -- `books` -- `memoir_images` 或后续统一 `assets` -- PDF 渲染与导出任务 - -### 4.5 Celery 批次:路由 + 仅 Story 落库 - -异步任务 `process_memoir_segments` 不再写入 `chapter_sections`。每个「章节类别 + 本批 segments」在 Celery 内走统一流水线(见 `story_pipeline_sync.run_story_pipeline_for_category_batch`): - -1. **检索**:`retrieve_evidence_sync` 提供 RAG evidence,与 transcript 合并为叙事输入(整批检索一次;可按 unit 复用同一份 evidence 文本)。 -2. **路由(批量规划)**:当同一章节类别下 **≥2 条 segment** 且未超过上限时,`StoryRouteAgent.plan_batch` 将本批 segment(带 id、按口述顺序)划分为若干 **连续块**,每块输出 `new_story` 或 `append_story`(`target_story_id` 必须来自候选列表),使「故事」与「可独立讲述的一段人生经历」对齐。解析或校验失败时回退为单次 `StoryRouteAgent.decide`(与旧行为一致)。**单条 segment** 直接走单次 `decide`,不额外调用批量规划。 -3. **叙事**:对每个写入单元,`NarrativeAgent` 仅接收该单元合并后的 transcript(+ evidence),产出 JSON/文本,经 `narrative_to_markdown` 规范为 markdown。 -4. **落库**:对每个单元依次 `create_story_with_version_sync` 或 `append_story_version_sync`,并 `ensure_chapter_story_link_sync` 将当前类别下的 active 章节与各 story 关联;最后 **`compose_chapter_from_story_links_sync` 调用一次** 物化 `chapters.canonical_markdown`。 -5. **后续**:commit 后派发 `generate_story_image`、`recompose_chapters_for_story`;章节封面由 `try_enqueue_generate_chapter_cover` → **`generate_chapter_cover`**(`chapter_cover_intents` + asset),正文插图由 `story_image_tasks` 消费 story 主图 intent。 - -对话侧多 Agent 仍负责访谈节奏与槽位;与 Celery 的衔接点是 segment 落库与异步任务,不共享同一套「写 story」代码路径。 - -## 5. 数据模型 - -### 5.1 现有保留表 - -保留并升级使用: - -- `conversations` -- `segments` -- `memory_sources` -- `memory_chunks` -- `memory_facts` -- `timeline_events` -- `memory_summaries` -- `books` - -### 5.2 新增主表 - -#### `stories` - -建议字段: - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| `id` | string | 主键 | -| `user_id` | string | 用户 ID | -| `title` | string | 故事标题 | -| `stage` | string | childhood / education / career / family / belief / summary | -| `story_type` | string | event / person / relationship / reflection / turning_point | -| `summary` | text | 短摘要 | -| `canonical_markdown` | text | 当前生效正文 | -| `time_start` | string/null | 起始时间,可为 year/month/date 粗粒度 | -| `time_end` | string/null | 结束时间 | -| `people_refs` | json | 人物引用 | -| `place_refs` | json | 地点引用 | -| `tag_refs` | json | 标签 | -| `status` | string | active / archived / merged / draft | -| `confidence` | float | story 聚合置信度 | -| `current_version_id` | string | 当前版本 | -| `created_at` | datetime | 创建时间 | -| `updated_at` | datetime | 更新时间 | - -#### `story_versions` - -建议字段: - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| `id` | string | 主键 | -| `story_id` | string | 所属 story | -| `version_no` | int | 递增版本号 | -| `markdown_snapshot` | text | 正文快照 | -| `change_summary` | text | 变更摘要 | -| `actor_type` | string | ai / user / editor / system | -| `source_type` | string | generate / rewrite / merge / manual / migration | -| `parent_version_id` | string/null | 父版本 | -| `prompt_meta` | json/null | 本次生成的 prompt / model / params 元信息 | -| `created_at` | datetime | 创建时间 | - -#### `story_evidence_links` - -建议字段: - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| `id` | string | 主键 | -| `story_id` | string | story | -| `evidence_type` | string | chunk / fact / timeline_event / summary | -| `evidence_id` | string | 证据 ID | -| `role` | string | primary / supporting / background | -| `weight` | float | 证据权重 | -| `created_at` | datetime | 创建时间 | - -### 5.3 重定义章节表 - -#### `chapters` - -建议字段: - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| `id` | string | 主键 | -| `user_id` | string | 用户 ID | -| `book_id` | string/null | 所属 book | -| `title` | string | 章节标题 | -| `category` | string | 章节分类 | -| `order_index` | int | 排序 | -| `summary` | text | 章节摘要 | -| `canonical_markdown` | text | 当前生效正文 | -| `status` | string | active / draft / archived | -| `cover_asset_id` | string/null | 封面资源 | -| `current_version_id` | string | 当前版本 | -| `created_at` | datetime | 创建时间 | -| `updated_at` | datetime | 更新时间 | - -#### `chapter_versions` - -结构与 `story_versions` 类似,保存章节正文版本。 - -#### `chapter_story_links` - -建议字段: - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| `id` | string | 主键 | -| `chapter_id` | string | chapter | -| `story_id` | string | story | -| `order_index` | int | 在章节中的顺序 | -| `role` | string | core / bridge / appendix | -| `created_at` | datetime | 创建时间 | - -### 5.4 废弃对象 - -上线后退出正文真源角色: - -- `chapter_sections` - -如需保留,仅允许作为短期迁移脚本输入,不进入新运行时。 - -## 6. Markdown 契约 - -### 6.1 决策 - -`markdown` 是唯一正文真源。 - -以下对象都必须持有 canonical markdown: - -- `stories.canonical_markdown` -- `chapters.canonical_markdown` - -### 6.2 允许语法 - -本阶段只允许安全且稳定的受限 markdown 子集: - -- `#`、`##` 标题 -- 段落 -- `>` 引用 -- `---` 分隔线 -- `**粗体**` -- `*斜体*` -- 简单无序列表 -- 图片:`![caption](asset://)` - -### 6.3 禁止语法 - -- 任意 HTML -- 脚注 -- 表格 -- 复杂嵌套列表 -- 任意外链资源 -- 自定义脚本或内联样式 - -### 6.4 图片规则 - -正文只存资源引用,不存真实 URL。 - -示例: - -```md -![老家门口的那条土路](asset://img_001) -``` - -解析时: - -- app 端根据 `asset_id` 拉取资源 -- PDF 渲染器根据 `asset_id` 注入图片 -- 图注默认来自 alt 文本 - -## 7. Multi-Agent 设计 - -### 7.1 原则 - -- orchestrator 决策 -- specialist agent 产出结构化结果 -- service/repo 落库 -- agent 禁止直接访问 ORM/DB - -### 7.2 新编排链路 - -```mermaid -flowchart TD - A["ConversationOrchestrator"] --> B["MemoryIngestOrchestrator"] - B --> C["ExtractionAgent"] - B --> D["Fact/Timeline Enrichment"] - D --> E["StoryBuilderOrchestrator"] - E --> F["StorySynthesisAgent"] - E --> G["StoryMergeAgent"] - E --> H["Story Version Write"] - H --> I["ChapterComposerOrchestrator"] - I --> J["ChapterOutlineAgent"] - I --> K["ChapterComposeAgent"] - I --> L["Chapter Version Write"] - L --> M["Reading / Export / Image"] -``` - -### 7.3 Orchestrator 职责 - -#### `ConversationOrchestrator` - -- 负责在线访谈 -- 使用 conversation RAG 做追问 grounding -- 输出 transcript / segments - -#### `MemoryIngestOrchestrator` - -- transcript 写入 `memory_sources` -- 切块写入 `memory_chunks` -- 触发 embedding / FTS / fact / timeline enrichment - -#### `StoryBuilderOrchestrator` - -- 判断新增 story、补充现有 story、合并重复 story -- 组织 evidence bundle -- 生成或更新 story markdown - -#### `ChapterComposerOrchestrator` - -- 读取 stories -- 生成章节大纲和章节 markdown -- 维护 `chapter_story_links` - -#### `ImageOrchestrator` - -- 从 story 或 chapter 上下文生成图片 prompt -- 只返回 asset 结果,不修改正文真源 - -### 7.4 Specialist Agents - -建议保留这些 specialist: - -- `ProfileAgent` -- `InterviewAgent` -- `ExtractionAgent` -- `StorySynthesisAgent` -- `StoryMergeAgent` -- `ChapterOutlineAgent` -- `ChapterComposeAgent` -- `PromptGenerationAgent` - -## 8. RAG 设计 - -### 8.1 两条读路径 - -#### `conversation RAG` - -用途: - -- 帮助访谈更具体 -- 避免重复追问 -- 把已讲过的人、地点、事件重新接回来 - -读取对象: - -- `stories` -- `memory_facts` -- `timeline_events` -- 必要时回到 `memory_chunks` - -#### `writing RAG` - -用途: - -- 支撑 story 生成 -- 支撑 chapter 编排 -- 约束忠实度与证据引用 - -读取对象: - -- `memory_chunks` -- `memory_facts` -- `timeline_events` -- `memory_summaries` -- `stories` - -### 8.2 检索策略 - -Retriever 采用混合检索: - -1. metadata filter -2. FTS -3. vector retrieval -4. score fusion -5. token budget 裁剪 - -输出统一 evidence bundle: - -```json -{ - "relevant_chunks": [], - "relevant_summaries": [], - "relevant_facts": [], - "timeline_hints": [], - "relevant_stories": [] -} -``` - -### 8.3 重要边界 - -- RAG 只读 evidence / story,不直接改正文。 -- story/chapter 生成结果必须落到 versioned markdown。 -- 事实层和正文层分离,避免“结构化事实污染叙事表达”。 - -## 9. API 契约 - -### 9.1 App 端主接口 - -#### `GET /api/chapters` - -返回: - -- `id` -- `title` -- `category` -- `order_index` -- `summary` -- `cover_asset` -- `updated_at` - -不返回 `sections`。 - -#### `GET /api/chapters/:id` - -返回: - -- `id` -- `title` -- `category` -- `canonical_markdown` -- `rendered_assets` -- `reading_meta` -- `updated_at` - -说明: - -- `canonical_markdown` 是正文真源 -- `rendered_assets` 是图片等资源映射 -- `reading_meta` 可包含估算阅读时长、目录信息、是否有未读更新等 - -#### `GET /api/books/current` - -继续服务目录与导出流程,但正文消费只认 chapter markdown。 - -### 9.2 未来运营端接口 - -预留,不进入当前 app 主流程: - -- `GET /api/stories/:id` -- `GET /api/stories/:id/versions` -- `POST /api/stories/:id/rewrite` -- `POST /api/chapters/:id/recompose` - -## 10. 阅读页设计约束 - -### 10.1 前端读取模型 - -`app-expo` 章节页从 `chapter.canonical_markdown` 渲染,不再从 `chapter.sections` 渲染。 - -### 10.2 渲染层 - -客户端可将 markdown 转换为受控 AST,再映射到原生阅读组件: - -- Heading -- Paragraph -- Quote -- Divider -- Image -- Caption - -### 10.3 渲染缓存 - -允许生成 render cache,例如: - -- 目录块 -- 首屏块 -- 阅读进度锚点 -- 图片资源索引 - -但 render cache 不是正文真源。 - -## 11. LLM Runtime 边界 - -### 11.1 决策 - -LangChain 可以保留,但只能留在 adapter/runtime 层。 - -禁止: - -- agent 直接拿 `langchain_llm` -- 业务逻辑耦合 LangChain message 类型 - -### 11.2 建议接口 - -统一抽象 `LLMRuntime`: - -- `generate_text()` -- `generate_json()` -- `stream_text()` -- `embed_texts()` - -底层实现可以继续包 LangChain / OpenAI-compatible provider,但对 agent 层隐藏实现细节。 - -## 12. 错误处理 - -错误按四层处理: - -### 12.1 `evidence failure` - -- transcript 保留 -- 允许后续重试 enrichment -- 不阻塞会话结束 - -### 12.2 `story synthesis failure` - -- 不覆盖当前 story 版本 -- 保留 evidence -- 标记待重试任务 - -### 12.3 `chapter compose failure` - -- 不影响已有章节阅读 -- 不污染已发布章节版本 - -### 12.4 `render failure` - -- app 回退纯文本模式 -- PDF 导出回退安全渲染 - -强约束: - -- 任何 AI 生成只新增 version -- 禁止原地覆盖当前生效版本 - -## 13. 测试策略 - -### 13.1 Golden Tests - -- transcript -> story markdown -- story set -> chapter markdown -- chapter markdown -> PDF snapshot - -### 13.2 Parser / Renderer Tests - -- markdown AST 解析 -- app-expo 阅读渲染 -- PDF 渲染一致性 -- 图片 asset 引用解析 - -### 13.3 Migration Tests - -- 旧 `chapter/section` -> 新 `story/chapter markdown` -- 数据量校验 -- 版本链校验 -- 资源引用校验 - -### 13.4 Contract Tests - -- API 不再返回 `sections` 真源 -- chapter detail 必须返回 `canonical_markdown` -- orchestrator 输出结构必须稳定 - -## 14. Cutover 方案 - -本次采用一次性切换,不保留旧运行时兼容。 - -### 14.1 切换顺序 - -1. 冻结旧 `chapter/section` 体系的新功能开发。 -2. 完成新 schema migration。 -3. 完成历史数据迁移: - - 旧 transcript/sections -> evidence - - 旧章节正文 -> story markdown / chapter markdown - - 旧图片 -> asset 引用 -4. 切换后端: - - 新 repo - - 新 service - - 新 orchestrator - - 新 API DTO -5. 切换 `app-expo` 阅读页到 markdown renderer。 -6. 切换 PDF 导出到 markdown-based rendering。 -7. 运行 verifier: - - schema check - - row count / relation check - - markdown parse check - - asset resolution check - - golden tests -8. 发布。 -9. 发布后封禁旧表写入,删除旧代码路径。 - -### 14.2 上线后状态 - -上线后系统内只存在一套真相: - -- evidence 真相:memory tables -- 正文真相:story/chapter canonical markdown + versions - -不再存在: - -- 旧 `section` 正文真源 -- 旧 chapter-only 创作链路 -- 旧 agent 直连 LangChain runtime 的模式 - -## 15. 实施清单 - -### 15.1 数据层 - -- 新增 `stories` -- 新增 `story_versions` -- 新增 `story_evidence_links` -- 重定义 `chapters` -- 新增 `chapter_versions` -- 新增 `chapter_story_links` -- 废弃 `chapter_sections` - -### 15.2 服务层 - -- 完成 `MemoryService.ingest_transcript` -- 完成 `HybridRetriever.retrieve` -- 新增 `StoryService` -- 重写 `MemoirService` - -### 15.3 Agent 层 - -- 重写 story/chapter 编排链 -- 统一 orchestrator 输出结构 -- agent 禁止直接落库 - -### 15.4 前端层 - -- 章节页切换到 markdown 渲染 -- 目录页继续按 chapter 展示 -- 图片按 asset 引用解析 - -### 15.5 导出层 - -- PDF 从 chapter markdown 渲染 -- 与 app 阅读风格保持基本一致 - -## 16. 最终判断 - -这次重构的核心不是“把章节页改好看”,而是把回忆录系统的内容真源彻底改正确。 - -正确的长期抽象应当是: - -- evidence 是事实与来源底座 -- story 是创作真源 -- chapter 是阅读视图 -- markdown 是正文真源 - -只要这四个点不再动摇,后续无论是运营端润色、自动重编排、版本审计、在线 RAG、PDF 导出,都会建立在稳定架构上,而不是继续给 `section-first` 体系补洞。 diff --git a/docs/plans/2026-03-21-multi-agent-convergence.md b/docs/plans/2026-03-21-multi-agent-convergence.md deleted file mode 100644 index a2bac2d..0000000 --- a/docs/plans/2026-03-21-multi-agent-convergence.md +++ /dev/null @@ -1,16 +0,0 @@ -# 2026-03-21 Multi-Agent Convergence(已完成) - -> 该计划已完成。为减少后续搜索和维护噪音,这里只保留结果摘要,不再保留迁移过程中的逐项旧路径说明。 - -## 收敛结果 - -- Chat 只保留 `ChatOrchestrator` 作为实时编排入口。 -- 会话历史只保留 `conversation_messages` 为 DB 真源,Redis 为缓存。 -- Memoir 只保留 `MemoirOrchestrator.prepare_batches` + `run_story_pipeline_for_category_batch` 主链路。 -- 图片只保留 `generate_story_image` 与 `generate_chapter_cover` 正式任务。 - -## 清理结果 - -- 旧 facade、旧 agent-layer runner、旧章节补图任务名均已移除。 -- 旧的双轨历史重建逻辑已删除。 -- 设计/计划文档已改成归档摘要,避免继续传播过时工作流。 diff --git a/docs/plans/2026-03-21-remove-memoir-compatibility-layers.md b/docs/plans/2026-03-21-remove-memoir-compatibility-layers.md deleted file mode 100644 index 6f239a8..0000000 --- a/docs/plans/2026-03-21-remove-memoir-compatibility-layers.md +++ /dev/null @@ -1,106 +0,0 @@ -# Memoir Compatibility Cleanup Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Remove remaining memoir stories-first compatibility layers so runtime only serves the current `story -> chapter` contract. - -**Architecture:** Tighten the backend around story-linked chapters only, remove transitional serializer fields and dead endpoints, then update Expo consumers to rely only on canonical markdown, reading segments, and cover assets. Keep fallback logic only where it protects current-state correctness rather than old schema compatibility. - -**Tech Stack:** FastAPI, SQLAlchemy, Alembic, Expo Router, React Query, TypeScript, Jest - ---- - -### Task 1: Remove obsolete backend runtime paths - -**Files:** -- Modify: `api/app/features/memoir/service.py` -- Modify: `api/app/features/memoir/router.py` -- Modify: `api/app/features/memoir/repo.py` -- Modify: `api/app/features/memoir/chapter_cover.py` -- Modify: `api/app/tasks/chapter_cover_tasks.py` -- Modify: `api/app/tasks/chapter_cover_enqueue.py` -- Modify: `api/app/features/memoir/cover_eligibility.py` - -**Step 1: Remove dead whole-chapter regeneration path** - -Delete `MemoirService.regenerate_chapter` and the `/api/chapters/{chapter_id}/regenerate` route because stories-first no longer supports a separate chapter rewrite pipeline. - -**Step 2: Stop returning placeholder chapters** - -Update `MemoirService.get_chapters` to return real active chapters only. Remove `placeholder_*` records and related `empty` compatibility status generation. - -**Step 3: Require story-linked chapters for chapter-cover generation** - -Remove chapter-content fallback prompt generation and refuse enqueue/generation when a chapter has no `story_links`. - -**Step 4: Remove unused non-story chapter write helpers** - -Delete old repo helpers kept for the removed chapter-generation flow if they no longer have call sites. - -### Task 2: Tighten chapter API serialization - -**Files:** -- Modify: `api/app/features/memoir/helpers.py` -- Modify: `api/app/features/memoir/models.py` -- Modify: `api/app/features/memoir/pdf_service.py` -- Modify: `api/alembic/versions/0001_initial_schema.py` - -**Step 1: Remove legacy response fields** - -Stop serializing `sections`, `content`, and `rendered_assets` from chapter payloads when those fields are no longer part of the stories-first contract. - -**Step 2: Remove `cover_image` JSON fallback usage** - -Drop runtime dependence on legacy `cover_image` snapshots and keep `cover_asset_id` as the only supported chapter-cover source. - -**Step 3: Simplify PDF generation inputs** - -Consume canonical markdown directly instead of section-based compatibility helpers. - -### Task 3: Update Expo memoir consumers - -**Files:** -- Modify: `app-expo/src/features/memoir/types.ts` -- Modify: `app-expo/src/features/memoir/mappers.ts` -- Modify: `app-expo/src/features/memoir/api.ts` -- Modify: `app-expo/src/app/(tabs)/memoir.tsx` -- Modify: `app-expo/src/app/(main)/chapter/[id].tsx` - -**Step 1: Remove old chapter fields from types** - -Delete `ChapterSection`, remove `sections` and `content` from chapter contracts, and simplify `ChapterViewModel`. - -**Step 2: Remove sections-based view model fallbacks** - -Compute emptiness and word count from canonical markdown and summary only. - -**Step 3: Remove dead UI branches** - -Delete unreachable locked variants and unused API methods tied to removed backend endpoints. - -### Task 4: Refresh tests and verify - -**Files:** -- Modify: `app-expo/tests/features/memoir/mappers.test.ts` - -**Step 1: Update mapper tests** - -Rewrite fixtures and assertions to match the new stories-first payload shape. - -**Step 2: Run focused verification** - -Run: - -```bash -cd /Users/timcook/Codes/hgtk/life-echo/app-expo && npm run test:ci -- --runTestsByPath tests/features/memoir/mappers.test.ts -``` - -Run: - -```bash -cd /Users/timcook/Codes/hgtk/life-echo/app-expo && npm run lint -- src/features/memoir src/app/\(tabs\)/memoir.tsx src/app/\(main\)/chapter/\[id\].tsx -``` - -Expected: -- Mapper tests pass -- Targeted Expo lint passes, or any remaining failures are unrelated and documented diff --git a/docs/plans/2026-03-22-clean-slate-schema-refactor.md b/docs/plans/2026-03-22-clean-slate-schema-refactor.md deleted file mode 100644 index 5973ae0..0000000 --- a/docs/plans/2026-03-22-clean-slate-schema-refactor.md +++ /dev/null @@ -1,1073 +0,0 @@ -# Clean Slate Schema Refactor Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Remove all backward-compatibility layers, collapse duplicate persistence paths, rename inconsistent schema/API vocabulary, and rebuild the backend around a single clean contract backed by one hand-written Alembic `0001_initial.py`. - -**Architecture:** Treat the product as five bounded contexts: identity/billing, conversation, memory, story, and memoir. Collapse duplicate conversation storage into a single `conversation_turns` truth source, collapse old memoir image flow into canonical `assets + story_image_intents + memoir_chapter_cover_intents`, and rename every remaining schema/API surface to one vocabulary. All schema creation must live in explicit Alembic DDL; runtime code may consume migrations but must never generate schema with `create_all`. - -**Tech Stack:** FastAPI, SQLAlchemy 2.x, Alembic, PostgreSQL 17, pgvector, Celery, Redis, Expo Router, React Query, TypeScript, Jest - ---- - -## Non-Negotiables - -- 执行时必须在独立 worktree 中进行,避免和当前脏工作区互相污染。 -- 允许删库重建,因此**禁止**写任何数据迁移、双写、回填兼容、deprecated alias。 -- 目标是单一路径,不保留旧表名、旧字段名、旧路由名、旧环境变量名。 -- `@architecture-patterns`:命名只反映 bounded context,不再混用 `book` / `memoir` / `image` / `asset` / `state`。 -- `@supabase-postgres-best-practices`:所有 FK、唯一约束、索引、部分索引都显式写在 Alembic 中,不依赖 ORM 隐式副作用。 -- `@verification-before-completion`:不允许“看起来差不多”;迁移测试、后端测试、前端测试、lint 必须实际跑过。 -- 完成后仓库中不得再出现: - - `Base.metadata.create_all(...)` - - 运行时自动 `alembic upgrade head` - - `memoir_images` 表 - - `api/app/features/memoir/memoir_images/` 包 - - `segments` 表 - - `conversation_messages` 表 - - `books` 表 - - `memoir_states` 表 - - `timeline_events` 表 - - `{{IMAGE:...}}` 或 `{{{{IMAGE:...}}}}` 占位符兼容渲染 - - `premium` 套餐 alias - -## Target Vocabulary - -### 数据库词典 - -| Current | Target | Action | Notes | -| --- | --- | --- | --- | -| `segments` | removed | delete | 合并进 `conversation_turns`,不再维护双表 | -| `conversation_messages` | `conversation_turns` | rename + widen | 单一对话真源,承载 human/ai turn 与音频元数据 | -| `timeline_events` | `memory_timeline_events` | rename | memory 域统一前缀 | -| `books` | `memoirs` | rename | 内容聚合根统一用 memoir | -| `chapters` | `memoir_chapters` | rename | 消除 generic `chapters` 歧义 | -| `chapter_versions` | `memoir_chapter_versions` | rename | 与上面保持一致 | -| `chapter_story_links` | `memoir_chapter_story_links` | rename | 与上面保持一致 | -| `chapter_cover_intents` | `memoir_chapter_cover_intents` | rename | 与上面保持一致 | -| `memoir_states` | `memoir_workflow_states` | rename | 明确这是流程态,不是内容实体 | -| `memoir_images` | removed | delete | 章节内联图片兼容层彻底移除;只保留 `assets` + intent | - -### 字段词典 - -| Current | Target | Action | -| --- | --- | --- | -| `users.subscription_type` | `users.plan_code` | rename | -| `users.subscription_expires_at` | `users.plan_expires_at` | rename | -| `orders.plan_id` | `orders.plan_code` | rename | -| `memoirs.cover_image_url` | `memoirs.cover_asset_id` | rename + FK | -| `memoirs.has_update` | `memoirs.has_unread_update` | rename | -| `memoirs.last_update_chapter_id` | `memoirs.last_changed_chapter_id` | rename | -| `memoir_chapters.book_id` | `memoir_chapters.memoir_id` | rename | -| `memoir_chapters.is_new` | `memoir_chapters.is_unread` | rename | -| `memoir_chapters.is_active` | removed | delete, use `status` only | -| `memoir_chapters.source_segments` | `memoir_chapters.source_turn_ids` | rename | -| `memoir_chapters.reading_segments_json` | `memoir_chapters.reading_segments_snapshot` | rename | - -### API 词典 - -| Current | Target | -| --- | --- | -| `GET /api/books/current` | `GET /api/memoirs/current` | -| `PUT /api/books/{book_id}` | `PUT /api/memoirs/{memoir_id}` | -| `POST /api/books/clear-update` | `POST /api/memoirs/clear-unread` | -| `POST /api/books/export-pdf` | `POST /api/memoirs/export-pdf` | -| `GET /api/chapters` | `GET /api/memoir-chapters` | -| `GET /api/chapters/{chapter_id}` | `GET /api/memoir-chapters/{chapter_id}` | -| `DELETE /api/chapters/{chapter_id}` | `DELETE /api/memoir-chapters/{chapter_id}` | -| `POST /api/chapters/check-cover-generation` | `POST /api/memoir-chapters/check-cover-generation` | -| `GET /api/memoir-state` | `GET /api/memoir-workflow` | -| `GET /api/memoir-state/next-question` | `GET /api/memoir-workflow/next-question` | -| `POST /api/memoir-state/mark-read` | `POST /api/memoir-workflow/clear-unread` | - -## Clean 0001 Target Tables - -最终 `alembic upgrade head` 后,public schema 只允许这 23 张表: - -```text -assets -conversation_turns -conversations -memory_chunks -memory_curation_actions -memory_facts -memory_sources -memory_summaries -memory_timeline_events -memoir_chapter_cover_intents -memoir_chapter_story_links -memoir_chapter_versions -memoir_chapters -memoir_workflow_states -memoirs -orders -refresh_tokens -sms_verification_codes -stories -story_evidence_links -story_image_intents -story_versions -users -``` - -必须不存在的旧表: - -```text -books -chapter_cover_intents -chapter_story_links -chapter_versions -chapters -conversation_messages -memoir_images -memoir_states -segments -timeline_events -``` - -## Alembic `0001_initial.py` Rules - -- 文件路径固定为 `api/alembic/versions/0001_initial.py` -- 删除 `api/alembic/versions/0001_initial_schema.py` -- `upgrade()` / `downgrade()` 都必须显式写 DDL -- 禁止 import models 后调用 `Base.metadata.create_all()` / `drop_all()` -- 先 `CREATE EXTENSION IF NOT EXISTS vector` -- `stories.current_version_id` 与 `memoir_chapters.current_version_id` 采用“两步建表 + 后置 FK”模式 -- 所有 FK 明确声明 `ondelete` -- 所有唯一约束、部分唯一索引、GIN 索引都在 migration 中显式写出 - -建议的建表顺序: - -1. `users` -2. `assets` -3. `refresh_tokens` -4. `sms_verification_codes` -5. `orders` -6. `conversations` -7. `conversation_turns` -8. `memory_sources` -9. `memory_chunks` -10. `memory_summaries` -11. `memory_facts` -12. `memory_timeline_events` -13. `memory_curation_actions` -14. `stories` -15. `story_versions` -16. `story_evidence_links` -17. `story_image_intents` -18. `memoirs` -19. `memoir_chapters` -20. `memoir_chapter_versions` -21. `memoir_chapter_story_links` -22. `memoir_chapter_cover_intents` -23. `memoir_workflow_states` -24. 追加 FK / unique / partial index / GIN index - -代表性 DDL 片段: - -```python -from alembic import op -import sqlalchemy as sa -from pgvector.sqlalchemy import Vector -from sqlalchemy.dialects import postgresql - -op.create_table( - "conversation_turns", - sa.Column("id", sa.String(), primary_key=True), - sa.Column( - "conversation_id", - sa.String(), - sa.ForeignKey("conversations.id", ondelete="CASCADE"), - nullable=False, - ), - sa.Column("role", sa.String(), nullable=False), - sa.Column("content", sa.Text(), nullable=False), - sa.Column("message_type", sa.String(), nullable=False, server_default="text"), - sa.Column("audio_url", sa.String(), nullable=True), - sa.Column("audio_duration_seconds", sa.Integer(), nullable=True), - sa.Column("tts_audio_urls", sa.JSON(), nullable=True), - sa.Column("voice_session_id", sa.String(), nullable=True), - sa.Column("topic_category", sa.String(), nullable=True), - sa.Column("processed_for_memoir", sa.Boolean(), nullable=False, server_default=sa.false()), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")), -) - -op.create_table( - "memory_chunks", - sa.Column("id", sa.String(), primary_key=True), - sa.Column("source_id", sa.String(), sa.ForeignKey("memory_sources.id", ondelete="CASCADE"), nullable=False), - sa.Column("user_id", sa.String(), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False), - sa.Column("content", sa.Text(), nullable=False), - sa.Column("embedding", Vector(1536), nullable=True), - sa.Column( - "content_tsv", - postgresql.TSVECTOR(), - sa.Computed("to_tsvector('simple', coalesce(content, ''))", persisted=True), - nullable=True, - ), - sa.Column("chunk_index", sa.Integer(), nullable=False), - sa.Column("metadata_json", sa.JSON(), nullable=True), - sa.Column("is_excluded", sa.Boolean(), nullable=False, server_default=sa.false()), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")), -) -op.create_index( - "ix_memory_chunks_content_tsv", - "memory_chunks", - ["content_tsv"], - postgresql_using="gin", -) - -op.execute( - ''' - CREATE UNIQUE INDEX uq_story_image_intents_primary_per_story - ON story_image_intents (story_id) - WHERE intent_role = 'primary' - ''' -) -``` - ---- - -### Task 1: Create a Dedicated Worktree and Backend Test Harness - -**Files:** -- Create: `api/tests/conftest.py` -- Create: `api/tests/helpers/postgres.py` -- Create: `api/tests/test_test_harness.py` -- Modify: `api/pyproject.toml` - -**Step 1: Create the clean-slate worktree** - -Run: - -```bash -cd /Users/timcook/Codes/hgtk/life-echo -git worktree add ../life-echo-clean-slate -b codex/clean-slate-schema -``` - -Expected: -- New worktree created at `/Users/timcook/Codes/hgtk/life-echo-clean-slate` - -**Step 2: Write the failing harness smoke test** - -```python -def test_test_database_url_is_configured(test_database_url: str) -> None: - assert test_database_url.startswith("postgresql://") -``` - -**Step 3: Run the test to verify it fails** - -Run: - -```bash -cd /Users/timcook/Codes/hgtk/life-echo-clean-slate/api -TEST_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/life_echo_test \ -uv run pytest tests/test_test_harness.py -v -``` - -Expected: -- FAIL because `tests/conftest.py` and fixture do not exist yet - -**Step 4: Write the minimal harness** - -```python -import os -import pytest - -@pytest.fixture -def test_database_url() -> str: - url = os.environ["TEST_DATABASE_URL"] - assert url - return url -``` - -**Step 5: Run the test to verify it passes** - -Run: - -```bash -cd /Users/timcook/Codes/hgtk/life-echo-clean-slate/api -TEST_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/life_echo_test \ -uv run pytest tests/test_test_harness.py -v -``` - -Expected: -- PASS - -**Step 6: Commit** - -```bash -git add api/tests/conftest.py api/tests/helpers/postgres.py api/tests/test_test_harness.py api/pyproject.toml -git commit -m "test: add backend integration test harness" -``` - -### Task 2: Collapse Duplicate Conversation Persistence into `conversation_turns` - -**Files:** -- Modify: `api/app/features/conversation/models.py` -- Modify: `api/app/features/conversation/repo.py` -- Modify: `api/app/features/conversation/service.py` -- Modify: `api/app/features/conversation/router.py` -- Modify: `api/app/features/conversation/history_store.py` -- Modify: `api/app/features/conversation/session_history.py` -- Modify: `api/app/features/conversation/ws/router.py` -- Modify: `api/app/features/conversation/ws/pipeline.py` -- Modify: `api/app/features/quota/service.py` -- Modify: `api/app/features/memoir/models.py` -- Modify: `api/app/features/memoir/repo.py` -- Modify: `api/app/features/memoir/service.py` -- Modify: `api/app/features/memoir/story_pipeline_sync.py` -- Modify: `api/app/tasks/memoir_tasks.py` -- Modify: `api/app/agents/memoir/orchestrator.py` -- Modify: `api/app/agents/memoir/story_route_agent.py` -- Test: `api/tests/features/conversation/test_turn_repository.py` -- Test: `api/tests/features/quota/test_quota_service.py` - -**Step 1: Write the failing tests** - -```python -def test_conversation_turns_are_the_only_chat_truth_source() -> None: - from app.features.conversation.models import ConversationTurn - - assert ConversationTurn.__tablename__ == "conversation_turns" - -def test_quota_counts_human_turns_only() -> None: - assert _countable_roles() == {"human"} -``` - -**Step 2: Run the tests to verify they fail** - -Run: - -```bash -cd /Users/timcook/Codes/hgtk/life-echo-clean-slate/api -uv run pytest tests/features/conversation/test_turn_repository.py tests/features/quota/test_quota_service.py -v -``` - -Expected: -- FAIL because `ConversationTurn` does not exist and quota still imports `Segment` - -**Step 3: Replace `Segment` + `ConversationMessage` with one model** - -```python -class ConversationTurn(Base): - __tablename__ = "conversation_turns" - - id = Column(String, primary_key=True) - conversation_id = Column(String, ForeignKey("conversations.id", ondelete="CASCADE"), nullable=False, index=True) - role = Column(String, nullable=False) # human / ai / system - content = Column(Text, nullable=False) - message_type = Column(String, nullable=False, default="text") - audio_url = Column(String, nullable=True) - audio_duration_seconds = Column(Integer, nullable=True) - tts_audio_urls = Column(JSON, nullable=True) - voice_session_id = Column(String, nullable=True) - topic_category = Column(String, nullable=True) - processed_for_memoir = Column(Boolean, default=False, nullable=False) - created_at = Column(DateTime(timezone=True), default=utc_now) -``` - -**Step 4: Update all conversation, quota, memoir, and task code paths** - -- Redis 历史重建只读 `conversation_turns` -- 配额统计只统计 `role == "human"` 的 turn -- `process_memoir_segments` 改名为 `process_memoir_turns` -- 章节来源字段统一改成 `source_turn_ids` -- 删除所有 `segment_id` / `segments_count` 语义,改为 `turn_id` / `turn_count` - -**Step 5: Run the tests to verify they pass** - -Run: - -```bash -cd /Users/timcook/Codes/hgtk/life-echo-clean-slate/api -uv run pytest tests/features/conversation/test_turn_repository.py tests/features/quota/test_quota_service.py -v -``` - -Expected: -- PASS - -**Step 6: Commit** - -```bash -git add api/app/features/conversation api/app/features/quota/service.py api/app/features/memoir api/app/tasks/memoir_tasks.py api/app/agents/memoir tests/features/conversation tests/features/quota -git commit -m "refactor: collapse conversation persistence into turns" -``` - -### Task 3: Normalize Billing Vocabulary Around `plan_code` - -**Files:** -- Modify: `api/app/features/user/models.py` -- Modify: `api/app/features/user/schemas.py` -- Modify: `api/app/features/user/service.py` -- Modify: `api/app/features/user/router.py` -- Modify: `api/app/features/auth/schemas.py` -- Modify: `api/app/features/auth/service.py` -- Modify: `api/app/features/auth/router.py` -- Modify: `api/app/features/plan/schemas.py` -- Modify: `api/app/features/plan/service.py` -- Modify: `api/app/features/quota/service.py` -- Modify: `api/app/features/payment/models.py` -- Modify: `api/app/features/payment/schemas.py` -- Modify: `api/app/features/payment/order_service.py` -- Modify: `api/app/features/payment/router.py` -- Modify: `app-expo/src/features/auth/types.ts` -- Modify: `app-expo/src/features/profile/types.ts` -- Modify: `app-expo/src/app/(tabs)/profile.tsx` -- Test: `api/tests/features/plan/test_plan_service.py` - -**Step 1: Write the failing tests** - -```python -def test_current_plan_response_uses_plan_code() -> None: - payload = build_current_plan_payload(plan_code="pro") - assert payload["plan_code"] == "pro" - assert "subscription_type" not in payload -``` - -**Step 2: Run the tests to verify they fail** - -Run: - -```bash -cd /Users/timcook/Codes/hgtk/life-echo-clean-slate/api -uv run pytest tests/features/plan/test_plan_service.py -v -``` - -Expected: -- FAIL because code still emits `subscription_type` - -**Step 3: Rename schema and service vocabulary** - -```python -class User(Base): - plan_code = Column(String, default="free") - plan_expires_at = Column(DateTime(timezone=True), nullable=True) - -class Order(Base): - plan_code = Column(String, nullable=False) -``` - -- 删除 `premium -> pro` alias -- 对外 API 响应统一用 `plan_code` -- 前端 profile/auth 类型同步改为 `plan_code` - -**Step 4: Run the tests to verify they pass** - -Run: - -```bash -cd /Users/timcook/Codes/hgtk/life-echo-clean-slate/api -uv run pytest tests/features/plan/test_plan_service.py -v -``` - -Run: - -```bash -cd /Users/timcook/Codes/hgtk/life-echo-clean-slate/app-expo -npm run lint -- src/features/auth src/features/profile src/app/\(tabs\)/profile.tsx -``` - -Expected: -- PASS - -**Step 5: Commit** - -```bash -git add api/app/features/user api/app/features/auth api/app/features/plan api/app/features/quota api/app/features/payment app-expo/src/features/auth app-expo/src/features/profile app-expo/src/app/'(tabs)'/profile.tsx -git commit -m "refactor: normalize billing vocabulary around plan code" -``` - -### Task 4: Normalize Memory and Story Schema Names - -**Files:** -- Modify: `api/app/features/memory/models.py` -- Modify: `api/app/features/memory/repo.py` -- Modify: `api/app/features/memory/retriever.py` -- Modify: `api/app/features/memory/timeline.py` -- Modify: `api/app/features/story/models.py` -- Modify: `api/app/features/story/repo.py` -- Modify: `api/app/features/story/service.py` -- Test: `api/tests/features/memory/test_schema_metadata.py` -- Test: `api/tests/features/story/test_story_models.py` - -**Step 1: Write the failing tests** - -```python -def test_memory_timeline_table_is_prefixed() -> None: - from app.features.memory.models import MemoryTimelineEvent - - assert MemoryTimelineEvent.__tablename__ == "memory_timeline_events" - -def test_story_image_intent_asset_fk_is_explicit() -> None: - assert story_image_intent_asset_fk_target() == "assets.id" -``` - -**Step 2: Run the tests to verify they fail** - -Run: - -```bash -cd /Users/timcook/Codes/hgtk/life-echo-clean-slate/api -uv run pytest tests/features/memory/test_schema_metadata.py tests/features/story/test_story_models.py -v -``` - -Expected: -- FAIL because `timeline_events` 仍是旧表名,且 FK 约束依赖迁移补丁而非模型语义 - -**Step 3: Rename the memory timeline model and tighten story relationships** - -```python -class MemoryTimelineEvent(Base): - __tablename__ = "memory_timeline_events" - -class StoryImageIntent(Base): - asset_id = Column(String, ForeignKey("assets.id", ondelete="SET NULL"), nullable=True) -``` - -- 同步更新 repo/retriever 查询 -- 保持 `story_*` 前缀完整一致 - -**Step 4: Run the tests to verify they pass** - -Run: - -```bash -cd /Users/timcook/Codes/hgtk/life-echo-clean-slate/api -uv run pytest tests/features/memory/test_schema_metadata.py tests/features/story/test_story_models.py -v -``` - -Expected: -- PASS - -**Step 5: Commit** - -```bash -git add api/app/features/memory api/app/features/story tests/features/memory tests/features/story -git commit -m "refactor: normalize memory and story schema names" -``` - -### Task 5: Refactor Memoir Domain to `memoirs` / `memoir_chapters` and Delete `memoir_images` - -**Files:** -- Modify: `api/app/features/memoir/models.py` -- Modify: `api/app/features/memoir/repo.py` -- Modify: `api/app/features/memoir/service.py` -- Modify: `api/app/features/memoir/router.py` -- Modify: `api/app/features/memoir/schemas.py` -- Modify: `api/app/features/memoir/helpers.py` -- Modify: `api/app/features/memoir/state_service.py` -- Modify: `api/app/features/memoir/chapter_cover.py` -- Modify: `api/app/features/memoir/cover_eligibility.py` -- Modify: `api/app/features/memoir/reading_segment_materialize.py` -- Modify: `api/app/features/memoir/pdf_service.py` -- Modify: `api/app/features/memoir/story_pipeline_sync.py` -- Modify: `api/app/features/user/models.py` -- Modify: `api/app/features/user/repo.py` -- Modify: `api/app/features/user/router.py` -- Test: `api/tests/features/memoir/test_serializers.py` -- Test: `api/tests/features/memoir/test_cover_eligibility.py` - -**Step 1: Write the failing tests** - -```python -def test_memoir_chapter_payload_has_no_legacy_image_list() -> None: - payload = serialize_memoir_chapter(sample_chapter()) - assert "images" not in payload - assert "is_new" not in payload - assert payload["is_unread"] is True - -def test_cover_eligibility_does_not_read_memoir_images() -> None: - assert cover_eligibility_source() == "story_links_and_cover_asset_only" -``` - -**Step 2: Run the tests to verify they fail** - -Run: - -```bash -cd /Users/timcook/Codes/hgtk/life-echo-clean-slate/api -uv run pytest tests/features/memoir/test_serializers.py tests/features/memoir/test_cover_eligibility.py -v -``` - -Expected: -- FAIL because serializers still emit `images` / `is_new`, and cover logic still imports `MemoirImage` - -**Step 3: Rename the memoir aggregate and chapter tables** - -```python -class Memoir(Base): - __tablename__ = "memoirs" - user_id = Column(String, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, unique=True, index=True) - title = Column(String, nullable=False) - total_pages = Column(Integer, default=0, nullable=False) - total_words = Column(Integer, default=0, nullable=False) - cover_asset_id = Column(String, ForeignKey("assets.id", ondelete="SET NULL"), nullable=True) - has_unread_update = Column(Boolean, default=False, nullable=False) - last_changed_chapter_id = Column(String, nullable=True) - -class MemoirChapter(Base): - __tablename__ = "memoir_chapters" - memoir_id = Column(String, ForeignKey("memoirs.id", ondelete="SET NULL"), nullable=True) - is_unread = Column(Boolean, default=True, nullable=False) - source_turn_ids = Column(JSON, nullable=True) - reading_segments_snapshot = Column(JSON, nullable=True) -``` - -**Step 4: Remove redundant memoir state fields and boolean drift** - -- 删除 `is_active` -- `DELETE /api/memoir-chapters/{id}` 改为只更新 `status="archived"` 或真正删除,二选一,只保留一种语义 -- `mark_read` 只更新 `is_unread` / `has_unread_update` - -**Step 5: Delete `MemoirImage` usage completely** - -- serializer 不再返回 `images` -- PDF 不再 strip placeholder 再插图 -- cover 生成只看 `story_links`、`cover_asset_id`、`memoir_chapter_cover_intents` -- 用户清理逻辑只删除 `assets.storage_key` - -**Step 6: Run the tests to verify they pass** - -Run: - -```bash -cd /Users/timcook/Codes/hgtk/life-echo-clean-slate/api -uv run pytest tests/features/memoir/test_serializers.py tests/features/memoir/test_cover_eligibility.py -v -``` - -Expected: -- PASS - -**Step 7: Commit** - -```bash -git add api/app/features/memoir api/app/features/user tests/features/memoir -git commit -m "refactor: normalize memoir aggregate and remove memoir images" -``` - -### Task 6: Extract Shared Asset and Image-Generation Infrastructure - -**Files:** -- Create: `api/app/features/asset/storage.py` -- Create: `api/app/features/asset/settings.py` -- Modify: `api/app/core/config.py` -- Modify: `api/app/core/dependencies.py` -- Modify: `api/app/tasks/story_image_tasks.py` -- Modify: `api/app/tasks/chapter_cover_tasks.py` -- Modify: `api/app/tasks/chapter_cover_enqueue.py` -- Modify: `api/app/features/memoir/helpers.py` -- Modify: `api/app/features/memoir/asset_urls.py` -- Modify: `api/app/features/memoir/asset_resolver.py` -- Delete: `api/app/features/memoir/memoir_images/__init__.py` -- Delete: `api/app/features/memoir/memoir_images/json_payload.py` -- Delete: `api/app/features/memoir/memoir_images/parser.py` -- Delete: `api/app/features/memoir/memoir_images/prompting.py` -- Delete: `api/app/features/memoir/memoir_images/schema.py` -- Delete: `api/app/features/memoir/memoir_images/serializers.py` -- Delete: `api/app/features/memoir/memoir_images/settings.py` -- Delete: `api/app/features/memoir/memoir_images/storage.py` -- Test: `api/tests/features/asset/test_storage.py` - -**Step 1: Write the failing tests** - -```python -def test_asset_storage_service_is_shared_not_memoir_specific() -> None: - from app.features.asset.storage import TencentCosStorageService - - assert TencentCosStorageService is not None -``` - -**Step 2: Run the tests to verify they fail** - -Run: - -```bash -cd /Users/timcook/Codes/hgtk/life-echo-clean-slate/api -uv run pytest tests/features/asset/test_storage.py -v -``` - -Expected: -- FAIL because `app.features.asset.storage` does not exist yet - -**Step 3: Move generic code into the asset feature** - -```python -@dataclass(frozen=True) -class ImageGenerationSettings: - enabled: bool = False - provider: str = "liblib" - default_style: str = "watercolor" - default_size: str = "1280x720" - poll_interval_seconds: int = 3 - max_attempts: int = 20 -``` - -- 环境变量统一改成 `image_generation_*` -- 删除 `memoir_image_max_per_chapter` / `memoir_image_chars_per_extra` / `memoir_image_max_cap` -- story image 与 chapter cover 都从 `asset.settings` 取配置 - -**Step 4: Run the tests to verify they pass** - -Run: - -```bash -cd /Users/timcook/Codes/hgtk/life-echo-clean-slate/api -uv run pytest tests/features/asset/test_storage.py -v -``` - -Expected: -- PASS - -**Step 5: Commit** - -```bash -git add api/app/features/asset api/app/core/config.py api/app/core/dependencies.py api/app/tasks/story_image_tasks.py api/app/tasks/chapter_cover_tasks.py api/app/tasks/chapter_cover_enqueue.py api/app/features/memoir -git commit -m "refactor: extract shared asset and image infrastructure" -``` - -### Task 7: Rewrite API Contracts and Expo Clients to the Clean Nouns - -**Files:** -- Modify: `api/app/features/memoir/router.py` -- Modify: `api/app/features/memoir/schemas.py` -- Modify: `api/app/features/plan/schemas.py` -- Modify: `api/app/features/auth/schemas.py` -- Modify: `api/app/features/user/schemas.py` -- Modify: `app-expo/src/features/memoir/api.ts` -- Modify: `app-expo/src/features/memoir/hooks.ts` -- Modify: `app-expo/src/features/memoir/types.ts` -- Modify: `app-expo/src/features/memoir/mappers.ts` -- Modify: `app-expo/src/features/memoir/markdown-renderer.tsx` -- Modify: `app-expo/src/app/(tabs)/memoir.tsx` -- Modify: `app-expo/src/app/(main)/chapter/[id].tsx` -- Modify: `app-expo/src/features/profile/api.ts` -- Modify: `app-expo/src/features/profile/types.ts` -- Modify: `app-expo/tests/features/memoir/mappers.test.ts` - -**Step 1: Write the failing frontend tests** - -```ts -test('memoir chapter contract has no renderedAssets fallback', () => { - const chapter = makeChapter(); - expect('images' in chapter).toBe(false); -}); -``` - -**Step 2: Run the tests to verify they fail** - -Run: - -```bash -cd /Users/timcook/Codes/hgtk/life-echo-clean-slate/app-expo -npm run test:ci -- --runTestsByPath tests/features/memoir/mappers.test.ts -``` - -Expected: -- FAIL because current types still require `images` - -**Step 3: Remove placeholder/image compatibility from the reader** - -```ts -export interface MemoirChapter { - id: string; - title: string; - order_index: number; - status: string; - category: string; - cover_asset: ImageAsset | null; - canonical_markdown?: string | null; - reading_segments?: ChapterReadingSegment[]; - is_unread: boolean; - source_turn_ids: string[]; - word_count?: number; - updated_at: string | null; -} -``` - -- 删除 `images` -- 删除 `placeholder` -- `MarkdownRenderer` 只渲染服务端已解析好的 markdown / image URL -- 前端 API 路径切到 `/api/memoirs/*`、`/api/memoir-chapters/*`、`/api/memoir-workflow/*` - -**Step 4: Run the tests to verify they pass** - -Run: - -```bash -cd /Users/timcook/Codes/hgtk/life-echo-clean-slate/app-expo -npm run test:ci -- --runTestsByPath tests/features/memoir/mappers.test.ts -``` - -Run: - -```bash -cd /Users/timcook/Codes/hgtk/life-echo-clean-slate/app-expo -npm run lint -- src/features/memoir src/features/profile src/app/\(tabs\)/memoir.tsx src/app/\(main\)/chapter/\[id\].tsx src/app/\(tabs\)/profile.tsx -``` - -Expected: -- PASS - -**Step 5: Commit** - -```bash -git add api/app/features/memoir api/app/features/plan api/app/features/auth api/app/features/user app-expo/src/features/memoir app-expo/src/features/profile app-expo/src/app/'(tabs)'/memoir.tsx app-expo/src/app/'(main)'/chapter/'[id]'.tsx app-expo/src/app/'(tabs)'/profile.tsx app-expo/tests/features/memoir/mappers.test.ts -git commit -m "refactor: align api and expo contracts with clean schema" -``` - -### Task 8: Replace the Alembic Baseline with an Explicit Clean `0001_initial.py` - -**Files:** -- Delete: `api/alembic/versions/0001_initial_schema.py` -- Create: `api/alembic/versions/0001_initial.py` -- Create: `api/tests/alembic/test_upgrade_head.py` -- Create: `api/tests/alembic/test_downgrade_base.py` - -**Step 1: Write the failing migration contract tests** - -```python -EXPECTED_TABLES = { - "assets", - "conversation_turns", - "conversations", - "memory_chunks", - "memory_curation_actions", - "memory_facts", - "memory_sources", - "memory_summaries", - "memory_timeline_events", - "memoir_chapter_cover_intents", - "memoir_chapter_story_links", - "memoir_chapter_versions", - "memoir_chapters", - "memoir_workflow_states", - "memoirs", - "orders", - "refresh_tokens", - "sms_verification_codes", - "stories", - "story_evidence_links", - "story_image_intents", - "story_versions", - "users", -} - -FORBIDDEN_TABLES = { - "books", - "chapters", - "chapter_versions", - "chapter_story_links", - "chapter_cover_intents", - "memoir_images", - "memoir_states", - "segments", - "conversation_messages", - "timeline_events", -} -``` - -**Step 2: Run the tests to verify they fail** - -Run: - -```bash -cd /Users/timcook/Codes/hgtk/life-echo-clean-slate/api -TEST_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/life_echo_test \ -uv run pytest tests/alembic/test_upgrade_head.py tests/alembic/test_downgrade_base.py -v -m integration -``` - -Expected: -- FAIL because current migration still creates old tables and uses `create_all` - -**Step 3: Write the explicit baseline** - -- `upgrade()` 中逐个 `op.create_table(...)` -- 显式写: - - `uq_memoirs_user_id` - - `uq_memoir_workflow_states_user_id` - - `uq_memoir_chapters_memoir_id_order_index` - - `uq_memoir_chapter_story_links_chapter_story` - - `uq_story_image_intents_primary_per_story`(partial unique index) - - `uq_orders_trade_no_not_null`(partial unique index) - - `ix_memory_chunks_content_tsv`(GIN) -- `downgrade()` 反向 drop,最后 `DROP EXTENSION IF EXISTS vector` - -**Step 4: Run the tests to verify they pass** - -Run: - -```bash -cd /Users/timcook/Codes/hgtk/life-echo-clean-slate/api -TEST_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/life_echo_test \ -uv run pytest tests/alembic/test_upgrade_head.py tests/alembic/test_downgrade_base.py -v -m integration -``` - -Expected: -- PASS -- Inspector 只看到目标 23 张表 -- Forbidden tables 全部不存在 - -**Step 5: Commit** - -```bash -git add api/alembic/versions/0001_initial.py api/tests/alembic -git rm api/alembic/versions/0001_initial_schema.py -git commit -m "refactor: replace schema baseline with explicit alembic 0001" -``` - -### Task 9: Remove Runtime Schema Bootstrap Debt and Centralize Model Registry - -**Files:** -- Create: `api/app/models.py` -- Modify: `api/alembic/env.py` -- Modify: `api/app/core/db.py` -- Modify: `api/app/main.py` -- Modify: `api/dev-up.sh` -- Modify: `api/README.md` -- Test: `api/tests/test_model_registry.py` - -**Step 1: Write the failing test** - -```python -def test_model_registry_imports_metadata_without_main_side_effects() -> None: - import app.models # noqa: F401 -``` - -**Step 2: Run the test to verify it fails** - -Run: - -```bash -cd /Users/timcook/Codes/hgtk/life-echo-clean-slate/api -uv run pytest tests/test_model_registry.py -v -``` - -Expected: -- FAIL because `app.models` does not exist - -**Step 3: Centralize metadata and remove runtime schema authorship** - -```python -# api/app/models.py -from app.features.asset import models as _asset_models -from app.features.auth import models as _auth_models -from app.features.conversation import models as _conversation_models -from app.features.memory import models as _memory_models -from app.features.memoir import models as _memoir_models -from app.features.payment import models as _payment_models -from app.features.story import models as _story_models -from app.features.user import models as _user_models -``` - -- 删除 `init_db_schema()` -- 删除 `startup_event()` 里的 `_run_alembic_upgrade()` -- `api/dev-up.sh` / 文档中明确要求先执行 `uv run alembic upgrade head` - -**Step 4: Run the test to verify it passes** - -Run: - -```bash -cd /Users/timcook/Codes/hgtk/life-echo-clean-slate/api -uv run pytest tests/test_model_registry.py -v -``` - -Expected: -- PASS - -**Step 5: Commit** - -```bash -git add api/app/models.py api/alembic/env.py api/app/core/db.py api/app/main.py api/dev-up.sh api/README.md api/tests/test_model_registry.py -git commit -m "refactor: remove runtime schema bootstrap debt" -``` - -### Task 10: Delete Dead Files, Refresh Docs, and Run Full Verification - -**Files:** -- Modify: `README.md` -- Modify: `api/README.md` -- Modify: `api/docs/README.md` -- Modify: `docs/数据库设计.md` -- Delete: any remaining dead imports/files revealed by `rg` - -**Step 1: Remove stale documentation and dead references** - -- README 中不再出现旧目录结构 `api/routers`, `api/services`, `app-android` 旧叙述 -- 文档中不再出现 `books`, `memoir-state`, `segments`, `conversation_messages`, `memoir_images` -- 补一节 “schema bootstrap policy: Alembic only” - -**Step 2: Run backend verification** - -Run: - -```bash -cd /Users/timcook/Codes/hgtk/life-echo-clean-slate/api -uv run ruff check app tests -``` - -Run: - -```bash -cd /Users/timcook/Codes/hgtk/life-echo-clean-slate/api -uv run pyright -``` - -Run: - -```bash -cd /Users/timcook/Codes/hgtk/life-echo-clean-slate/api -TEST_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/life_echo_test \ -uv run pytest -v -``` - -Expected: -- PASS - -**Step 3: Run frontend verification** - -Run: - -```bash -cd /Users/timcook/Codes/hgtk/life-echo-clean-slate/app-expo -npm run lint -``` - -Run: - -```bash -cd /Users/timcook/Codes/hgtk/life-echo-clean-slate/app-expo -npm run test:ci -``` - -Expected: -- PASS - -**Step 4: Commit** - -```bash -git add README.md api/README.md api/docs/README.md docs/数据库设计.md -git commit -m "docs: refresh architecture and schema documentation" -``` - -## Final Definition of Done - -- DB 只由 `api/alembic/versions/0001_initial.py` 建出目标 23 张表 -- 仓库中不存在旧 schema / API / env / markdown placeholder 兼容路径 -- Conversation 只有一个持久化真源:`conversation_turns` -- Memoir 图像链路只剩: - - `assets` - - `story_image_intents` - - `memoir_chapter_cover_intents` -- Billing 只有一个套餐词汇:`plan_code` -- FastAPI 启动不再偷偷迁移数据库 -- Expo 不再消费 `images`、`placeholder`、`subscription_type`、`/api/books/*` - -## Risks to Watch Explicitly - -- Conversation turn 合并是本次最大改动面,必须先补测试再动 WS / Celery -- 删除 `memoir_images` 后,任何仍依赖 placeholder 的渲染都会直接坏;前后端必须同批落地 -- 去掉启动时自动迁移后,开发脚本和部署脚本如果不补齐,会导致“代码已改、库没升级”的新故障 -- `plan_code` 重命名会波及 auth/profile/payment/quota,多端接口要一起改,不要留 alias - diff --git a/docs/plans/backend-architectural-refactor-plan.md b/docs/plans/backend-architectural-refactor-plan.md deleted file mode 100644 index 1ee5623..0000000 --- a/docs/plans/backend-architectural-refactor-plan.md +++ /dev/null @@ -1,865 +0,0 @@ ---- -name: 架构重构计划 -overview: 将现有 Life Echo API 从混合分层结构重构为 feature-first flat 架构,引入 ports/adapters 实现 provider-agnostic 基础设施抽象,补齐 code-first OpenAPI 契约治理,并新增面向回忆录生成的 Memory + RAG 能力,集成 Alembic 做 schema 变更治理,统一日志到 loguru。 -todos: - - id: phase0-core - content: "Phase 0: 搭建 app/core/ 骨架(config, db, logging, errors, security, dependencies, middleware)" - status: pending - - id: phase0-txn - content: "Phase 0: 重写 get_async_db() 去掉自动 commit,定义事务边界规则" - status: pending - - id: phase0-alembic - content: "Phase 0: 初始化 Alembic,手写空 baseline,stamp head(不依赖 autogenerate)" - status: pending - - id: phase0-loguru - content: "Phase 0: 集成 loguru + InterceptHandler + request_id middleware" - status: pending - - id: phase0-errors - content: "Phase 0: 实现全局异常体系 + exception handlers(仅错误响应统一,成功响应不包装)" - status: pending - - id: phase0-openapi - content: "Phase 0: 建立 OpenAPI 契约规范(router metadata、custom_openapi、内部接口可见性、WebSocket 协议文档)" - status: pending - - id: phase1-ports - content: "Phase 1: 定义 7 个核心 port protocol(sms, llm, image_gen, storage, tts, asr, embedding)" - status: pending - - id: phase1-embedding - content: "Phase 1: 新增 embedding port(可选预留 reranker port)并完成 DI 装配" - status: pending - - id: phase1-adapters - content: "Phase 1: 封装现有 SDK 代码为 adapter 实现" - status: pending - - id: phase1-di - content: "Phase 1: 在 core/dependencies.py 注册 DI factory" - status: pending - - id: phase2-content - content: "Phase 2: 迁移 content feature(faq, legal, home)" - status: pending - - id: phase2-plan - content: "Phase 2: 迁移 plan feature" - status: pending - - id: phase2-quota - content: "Phase 2: 提取 quota 为独立 feature(QuotaService + router)" - status: pending - - id: phase2-user - content: "Phase 2: 迁移 user feature(User model)" - status: pending - - id: phase2-auth - content: "Phase 2: 迁移 auth feature(RefreshToken + SmsVerificationCode models,依赖 user + sms port)" - status: pending - - id: phase2-memory - content: "Phase 2: 新增 memory feature(sources/chunks/summaries/facts/timeline + hybrid retrieval)" - status: pending - - id: phase2-payment - content: "Phase 2: 迁移 payment feature" - status: pending - - id: phase2-memoir - content: "Phase 2: 迁移 memoir feature(books + chapters + memoir_state 合并)" - status: pending - - id: phase2-conversation - content: "Phase 2: 迁移 conversation feature + WebSocket 拆为 ws/ 子包(6 个文件)" - status: pending - - id: phase3-cleanup - content: "Phase 3: 清理旧目录,更新 Docker/CI/测试路径" - status: pending -isProject: false ---- - -# Life Echo API 架构重构计划 - -## 一、现状诊断 - -当前项目位于 `api/` 下,采用混合分层结构: - -``` -api/ - main.py - routers/ # 16 个路由文件,部分含大量业务逻辑 - services/ # 混合了业务服务和外部 SDK 调用 - database/ # 单文件 models.py(9 个模型)+ database.py - agents/ # AI agent 层(conversation, memory, memoir) - middleware/ # 仅 auth.py - payment/ # 唯一较完整的 feature 模块 - tasks/ # Celery 后台任务 - migrations/ # 手写 SQL,无版本管理 -``` - -**核心问题**: - -- 路由层过胖,直接操作 DB 和调用外部服务 -- 外部服务耦合:SMS(腾讯云)、存储(腾讯 COS)、TTS(OpenAI)直接写死 SDK -- `memory` 语义不清:当前更像“聊天助手记忆”占位,缺少面向回忆录生产的素材沉淀、事实抽取、检索增强与来源追溯 -- 无统一配置类,`os.getenv()` 散落各处 -- 无 Alembic,手写 SQL 迁移无版本追踪 -- 标准库 logging,无结构化日志 -- 无全局异常处理 -- WebSocket 路由 ~1000 行,职责过重 - -**已有较好抽象的部分**(可复用/迁移): - -- `payment/`:已有 service facade + 双 provider -- `services/llm_service.py`:通过 LangChain 已做 provider 切换 -- ASR:已有 provider 切换机制 - ---- - -## 二、目标目录结构 - -``` -api/ - app/ - main.py # 仅做组装:include_router, middleware, lifespan, OpenAPI wiring - core/ - config.py # 统一 BaseSettings(pydantic-settings) - db.py # Base, engine, session factory, get_async_db - logging.py # loguru 配置 - errors.py # 全局异常体系 + exception handlers - security.py # JWT 签发/校验、密码哈希 - middleware.py # CORS, request_id, logging middleware - dependencies.py # 全局共享依赖(get_current_user, get_xxx_service, port DI factory) - openapi.py # custom_openapi():全局 schema 增强(title/version/logo/external docs) - pagination.py # 通用分页 - - features/ - auth/ - router.py # 注册、登录、SMS 登录、刷新、登出 - schemas.py - service.py # 认证业务逻辑 - models.py # RefreshToken, SmsVerificationCode - repo.py - deps.py # feature 特有依赖(如 sms_port 注入) - - conversation/ - router.py # 对话 CRUD - schemas.py - service.py # 对话编排(调用 agent、quota 检查等) - models.py # Conversation, Segment - repo.py - ws/ # WebSocket 子包(从 1067 行 websocket.py 拆分) - router.py # WebSocket endpoint,仅做连接生命周期 - connection_manager.py # 连接注册/注销、active_connections 字典 - message_types.py # MessageType 枚举 + 消息序列化 - pipeline.py # 消息分发管道:audio_segment -> ASR -> agent -> TTS -> 响应 - profile_collector.py # 资料收集模式:缺失字段检测、提取、回填 - quota_guard.py # 配额校验 hook(从 routers/quota 独立) - protocol.md # WebSocket 消息契约:message/event/error schema + reconnect/idempotency 规则 - - memoir/ - router.py # books, chapters, memoir_state 合并 - schemas.py - service.py # 回忆录编排(章节生成、状态流转) - models.py # Book, Chapter, MemoirState - repo.py - processor.py # 从 agents/memoir_processor.py 迁入 - - memory/ - router.py # 可选:后台编辑/管理接口(二期);一期以内部服务为主 - schemas.py - service.py # MemoryService:conversation / memoir 的统一门面 - repo.py - models.py # Source, Chunk, Summary, Fact, TimelineEvent, CurationAction - retriever.py # metadata filter + FTS + vector retrieval + score fusion - chunker.py # transcript -> chunks - extractor.py # entity/event/fact extraction - summarizer.py # session / rolling summaries - timeline.py # chronology organization - curation.py # exclude / restore / correct / reject / confirm - deps.py - - payment/ - router.py - schemas.py - service.py # 统一支付门面(已有良好抽象,迁移即可) - models.py # Order - repo.py - deps.py - - user/ - router.py # profile, subscription, feedback - schemas.py - service.py - models.py # User - repo.py - - quota/ - service.py # PLAN_QUOTAS 配置 + check_can_send/check_can_organize - schemas.py # QuotaCheckResponse - router.py # GET /api/quota/check - deps.py # get_quota_service 注入 - - plan/ - router.py - schemas.py - service.py - - content/ - router.py # faq, legal, home(轻量静态内容) - schemas.py - - agents/ - conversation_agent.py - prompts/ - conversation_prompts.py - profile_prompts.py - - ports/ # 能力接口定义(Protocol / ABC) - sms.py # SmsSender protocol - llm.py # LLMProvider protocol - image_gen.py # ImageGenerator protocol - storage.py # ObjectStorage protocol - tts.py # TTSProvider protocol - asr.py # ASRProvider protocol - embedding.py # EmbeddingProvider protocol - reranker.py # 可选:二期 cross-encoder rerank - - adapters/ # 具体 provider 实现 - sms/ - tencent.py - llm/ - deepseek.py # 基于现有 llm_service.py - image_gen/ - liblib.py - storage/ - tencent_cos.py - tts/ - openai_tts.py - asr/ - whisper_local.py - tencent_asr.py - embedding/ - openai.py - payment/ - wechat.py - alipay.py - - tasks/ - celery_app.py - memoir_tasks.py - memory_tasks.py - - alembic/ - env.py - versions/ - alembic.ini - - tests/ - ... -``` - -### 目录结构修订说明 - -**修订 1:`core/db.py` 显式列出。** 存放 `Base = declarative_base()`、engine/session factory、`get_async_db` 生成器。所有 feature models 共享此 Base。 - -**修订 2:`core/response.py` 移除。** 统一响应包装只用于错误响应(在 `core/errors.py` 的 exception handler 中处理)。成功响应直接返回 Pydantic model / `FileResponse` / 原始 dict,不强制 `{code, data, message}` 包装,避免对 FastAPI schema 自动生成、文件下载、支付回调、WebSocket 造成额外负担。 - -**修订 3:auth models 与 user models 分离。** `RefreshToken` 和 `SmsVerificationCode` 归入 `features/auth/models.py`,因为它们是 identity/session 资产,认证流程 (`auth/service.py`) 是唯一强依赖方。`User` 模型留在 `features/user/models.py`,auth 通过 import user models 引用 User(允许的依赖方向:auth -> user)。 - -**修订 4:WebSocket 从单个 `ws_handler.py` 拆为 `ws/` 子包。** 当前 `routers/websocket.py` 有 1067 行,同时承担连接管理、消息类型定义、音频分段处理、ASR 转写、配额校验、用户资料收集、Agent 编排、DB 落库、Celery 任务提交。仅改名为 `ws_handler.py` 会原样搬运复杂度。拆分为 6 个文件,每个职责单一。 - -**修订 5:`quota` 提升为独立 feature。** 当前 `routers/quota.py` 包含 `PLAN_QUOTAS` 配置、`get_segment_count`、`get_chapter_count`、`check_can_send_message`、`check_can_submit_organize`,被 conversation(WebSocket line 628)、memoir(任务提交前 line 1050)、plan 共同依赖。放在任何单个 feature 内都会造成反向依赖。独立为 `features/quota/`,其他 feature 的 service 通过注入 `QuotaService` 调用。 - -**修订 6:router 同时承担 OpenAPI 契约职责。** router 依然保持“薄”,不承载业务逻辑;但所有 HTTP 契约元数据必须定义在 path operation decorator 或 `APIRouter(...)` 上,包括 `response_model`、`status_code`、`responses`、`summary`、`description`、`tags`、必要时的 `operation_id` / `openapi_extra`。FastAPI 的 OpenAPI schema 是根据注册到 `app.routes` 的 path operations 自动生成的,因此契约不能下沉到 service。 - -**修订 7:WebSocket 协议独立成文。** WebSocket endpoint 可以继续使用 FastAPI 依赖注入做鉴权与资源装配,但其消息协议不会自然进入 HTTP OpenAPI。`conversation/ws/` 除代码拆分外,必须额外维护 `protocol.md` 或等价文档,显式描述 client -> server、server -> client、错误消息、状态流转、鉴权、重连与幂等等规则。 - -**修订 8:`memory` 从 agent 占位收敛为独立 feature。** 本项目中的 memory 不再按“聊天助手长期记忆”设计,而是定义为“面向回忆录生产的素材检索与事实组织系统”:原始口述素材沉淀 + 结构化事实抽取 + RAG 检索增强生成。它服务于跨会话素材召回、人物/事件/时间线组织、章节生成 grounding 与来源追溯。 - -**修订 9:`memory` 优先做基础设施与内部服务,不把后台编辑 API 作为一期前置条件。** Phase 2 先完成 `MemoryService`、数据模型、ingest/retrieve 流程、异步任务与 conversation/memoir 接入;管理端/编辑端接口保留在 `memory/router.py` 作为二期扩展位。 - ---- - -## 三、事务边界与 Session 管理 - -### 3.1 现状问题 - -当前存在两种矛盾的事务策略: - -- `get_async_db()` 在请求结束时自动 commit([database.py line 76](api/database/database.py)),但 router 内部又有显式 `await db.commit()`(如 [auth.py line 194](api/routers/auth.py)),导致双重 commit 或语义不清。 -- Celery 任务直接用同步 `SessionLocal()`([memoir_tasks.py line 18](api/tasks/memoir_tasks.py)),手动管理 session 生命周期,无统一规则。 -- WebSocket handler 用 `async for db in get_async_db()` 获取独立 session([websocket.py line 276](api/routers/websocket.py)),绕过 FastAPI 依赖注入。 - -### 3.2 新规则 - -**核心原则:repo 不提交,service 统一管理事务边界。** - -- `core/db.py` 中的 `get_async_db()` 改为"干净 session":只负责创建和关闭,不自动 commit/rollback。 -- 事务控制权交给 service 层:service 方法内显式 `await db.commit()` 或使用 `async with db.begin():` 块。 -- `repo.py` 只做查询和 `db.add()` / `db.delete()`;禁止调用 `commit()` / `rollback()`。`flush()` 不是常规手段,但在需要提前拿主键或尽早触发约束检查时允许受控使用,优先由 service 显式决定是否触发。 -- router 层不接触 session,通过 `Depends` 注入 service(service 内部持有 session)。 - -**Celery 任务**:在 `tasks/` 中定义 `get_sync_db()` context manager,同样遵循"service commit"规则。任务函数调用 service 的同步版本或直接在任务函数中管理事务。 - -**WebSocket**:`ws/pipeline.py` 中每个消息处理周期获取独立 session(通过 `async with AsyncSessionLocal() as session:`),处理完成后由 pipeline 显式 commit。 - -```python -# core/db.py - 新的 session 生成器 -async def get_async_db() -> AsyncGenerator[AsyncSession, None]: - async with AsyncSessionLocal() as session: - try: - yield session - finally: - await session.close() - -# features/auth/service.py - service 管理事务 -class AuthService: - def __init__(self, db: AsyncSession, sms: SmsSender): - self._db = db - self._sms = sms - - async def register(self, request: RegisterRequest) -> TokenResponse: - user = User(...) - self._db.add(user) - refresh_token = RefreshToken(...) - self._db.add(refresh_token) - await self._db.commit() - await self._db.refresh(user) - return TokenResponse(...) -``` - ---- - -## 四、依赖方向规则 - -用以下有向图描述允许的依赖方向(箭头 = "依赖于"): - -```mermaid -graph TD - Router["features/*/router.py"] --> Service["features/*/service.py"] - Service --> Repo["features/*/repo.py"] - Service --> Ports["ports/*"] - Service --> QuotaSvc["features/quota/service.py"] - ConversationSvc["conversation/service.py"] --> MemorySvc["memory/service.py"] - MemoirSvc["memoir/service.py"] --> MemorySvc - MemorySvc --> MemoryRepo["memory/repo.py"] - MemorySvc --> EmbeddingPort["ports/embedding.py"] - Repo --> Models["features/*/models.py"] - Adapters["adapters/*"] -.->|"实现"| Ports - Core["core/*"] --> Nothing["(无外部依赖)"] - Router --> Core - Service --> Core - Tasks["tasks/*"] --> Service - Agents["agents/*"] --> Ports - Agents -.->|"只消费 evidence bundle / ports,不直接读写 memory 表"| MemorySvc - WsPipeline["conversation/ws/pipeline.py"] --> Service - WsPipeline --> Ports -``` - - - -### 4.1 Service 装配规则 - -`core/dependencies.py` 和各 feature 的 `deps.py` 不只是放 token/user 依赖,也负责把 FastAPI dependency graph 延伸到 service 装配。router 不手工拼装 `db + adapter + settings`,而是统一依赖 `get_xxx_service()`。 - -```python -def get_auth_service( - db: Annotated[AsyncSession, Depends(get_async_db)], - sms: Annotated[SmsSender, Depends(get_sms_sender)], -) -> AuthService: - return AuthService(db=db, sms=sms) - - -@router.post( - "/login", - response_model=TokenResponse, - status_code=status.HTTP_200_OK, - responses={401: {"model": ErrorResponse}}, -) -async def login( - request: LoginRequest, - service: Annotated[AuthService, Depends(get_auth_service)], -) -> TokenResponse: - return await service.login(request) -``` - -这样可以直接复用 FastAPI 对函数/类依赖、sub-dependencies、`yield` 资源清理和类型驱动文档生成的能力。 - -**硬性禁止**: - -- `router.py` 不得直接 import adapter 或操作 DB session -- `service.py` 不得 import 具体 adapter,只依赖 port protocol -- `ports/` 不得 import `adapters/`(方向反转) -- feature 之间不得互相 import router;跨 feature 调用通过 service 注入 -- `repo.py` 不得调用 `commit()` / `rollback()`;`flush()` 仅允许在明确需要提前拿 ID / 触发约束检查时受控使用 -- `conversation` / `memoir` 不得直接读写 memory 表,只能调用 `MemoryService` -- `agents/` 不得直接读写 memory 表,只能消费 `MemoryService` 产出的 evidence bundle 或通过 port 获取能力 - ---- - -## 五、OpenAPI 契约与文档治理 - -### 5.1 Router 是 OpenAPI 主战场 - -每个 HTTP path operation 至少应在 router decorator 上明确声明以下信息中的适用项: - -- `response_model` -- `status_code` -- `responses` -- `summary` -- `description` -- `tags` - -如需扩展再使用: - -- `operation_id`:只有要做 SDK 生成或需要稳定外部契约名时才手工维护 -- `openapi_extra`:仅用于少量自定义扩展字段,避免把业务元数据塞进 service 或 schema 生成后处理 -- `include_in_schema=False`:明确隐藏内部接口,而不是靠“没人知道 URL” - -router 可以保持“薄”,但绝不能失去契约职责;service 负责业务,router 负责 HTTP 与 OpenAPI 边界。 - -### 5.2 Router 级共享元数据 - -每个 feature 的 `APIRouter(...)` 默认应收敛以下共享信息: - -- `prefix` -- `tags` -- 共享 `responses`(如 401、403、404、429) -- 必要时的通用 `dependencies` - -这样 feature boundary 会自然映射为文档 boundary,减少 decorator 重复并保证错误响应口径一致。 - -### 5.3 全局 OpenAPI 只做增强,不替代路由声明 - -在 `core/openapi.py` 中实现 `custom_openapi()`,内部通过 `get_openapi(..., routes=app.routes)` 基于已注册的 path operations 生成 schema,并只做全局增强,例如: - -- `title` / `version` / `summary` / `description` -- `x-logo` -- `externalDocs` -- tags 描述与排序 - -不要把正常应该写在 router 上的 `response_model`、`responses`、`summary` 等信息挪到 `custom_openapi()` 里“补”出来。 - -### 5.4 OpenAPI 版本与可见性策略 - -- 默认接受 FastAPI 生成的 OpenAPI 3.1.0 -- 只有某个代码生成器、网关或遗留工具明确不兼容 3.1 时,才考虑覆盖 `openapi_version` -- 内部管理接口、回调探针或调试端点若不应暴露在文档中,使用 `include_in_schema=False` - -### 5.5 WebSocket 协议单独治理 - -FastAPI 支持在 WebSocket endpoint 中继续使用 `Depends`、`Query`、`Cookie`、`Header` 等依赖能力,但 WebSocket 消息协议本身不应指望由 HTTP OpenAPI 自动表达。因此 `features/conversation/ws/` 需要额外维护一份协议文档,至少覆盖: - -- client -> server message schema -- server -> client event schema -- 错误消息 schema -- 鉴权方式 -- 状态流转 -- reconnect / idempotency / sequence 规则 - ---- - -## 六、Memory + RAG 增补方案 - -### 6.1 定位与设计原则 - -本项目中的 `memory` 不等于 chat history,也不等于 persona memory。它的产品定义应收敛为: - -- 面向回忆录生产的可检索素材资产 -- 可持续抽取、修正、确认的结构化事实层 -- 为章节生成和追问生成提供 grounding 的 RAG 基础设施 - -明确不做: - -- 通用“记住这个”的终端用户指令式记忆 -- 聊天助手式 persona memory -- 面向终端用户的自然语言 `forget that` - -一期目标是先打通内部基础设施:`MemoryService`、混合检索、摘要/事实/时间线分层存储、异步抽取与 evidence bundle 供给;管理端编辑 API 作为二期扩展,不作为 Phase 2 前置。 - -### 6.2 架构选择:SQL 事实层 + 摘要层 + Hybrid Retrieval - -`memory` 不应做成“只有向量库”的方案,推荐采用: - -- 原始素材层:保存 transcript / note / draft 等 truth source -- 摘要层:沉淀 session / rolling summary,降低长上下文重复注入成本 -- 结构化事实层:人物、事件、关系、地点、里程碑等可确认事实 -- 时间线层:面向 memoir 组织与 chronology 视图 -- 混合检索层:metadata filter + PostgreSQL FTS + pgvector semantic retrieval + score fusion - -根据 `pgvector` 文档,向量可直接与业务字段共存在 Postgres 中,并与普通 SQL filter 组合;根据 PostgreSQL 文档,FTS 可通过 `tsvector` + `GIN` index + `ts_rank/ts_rank_cd` 获得可控全文检索与排序能力。因此,推荐统一落在 PostgreSQL 中,以 `pgvector` 承担向量召回,以 `tsvector` / `GIN` 承担关键词与短语召回,再由 service 侧做融合与 rerank。 - -### 6.3 数据模型分层 - -`memory/models.py` 一期建议至少包含以下表: - -- `memory_sources`:原始素材主记录,保存 `source_type`、`raw_text`、`storage_key`、`speaker`、`captured_at`、`status` -- `memory_chunks`:检索单元,保存 `content`、`content_tsv`、`embedding`、`chunk_index`、`speaker`、`event_year`、`metadata_json`、`is_excluded` -- `memory_summaries`:会话摘要 / 滚动摘要 / 主题摘要 -- `memory_facts`:候选事实与已确认事实,包含 `fact_type`、`subject`、`predicate`、`object_json`、`confidence`、`source_chunk_id`、`status` -- `timeline_events`:时间线事件视图,包含 `event_year`、`event_date`、`person_refs`、`source_fact_ids` -- `memory_curation_actions`:记录 `exclude` / `restore` / `correct` / `merge` / `confirm` / `reject` 等操作轨迹 - -设计约束: - -- 原始素材、摘要、事实、时间线必须分层存储,不能把所有语义都压进单张向量表 -- 每条 summary / fact / timeline event 必须能追溯到 `source` 或 `chunk` -- `memory_chunks.embedding` 使用 `pgvector` -- `memory_chunks.content_tsv` 使用 PostgreSQL FTS(建议 generated / maintained `tsvector` 列 + `GIN` index) - -### 6.4 写入路径(Ingest) - -核心写入路径分两类: - -- 路径 A:对话口述沉淀。对话完成后,transcript 落 `memory_sources`,再切块写入 `memory_chunks`,随后异步生成 embedding / summary / facts / timeline -- 路径 B:人工导入素材。照片注释、手稿、家谱、旧文档、人工补录文本走同一套 source -> chunk -> enrich 流程 - -`MemoryService.ingest_transcript()` 建议按阶段编排: - -1. 写入 `source` -2. 切块并批量写入 `chunks` -3. 提交主事务 -4. 触发异步任务补齐 embedding、summary、fact extraction、timeline build - -这样可以避免把重型 AI 处理塞进实时链路,同时保持来源可追溯与失败可重试。 - -### 6.5 读取路径(Retrieve)与 Evidence Bundle - -章节生成、对话追问、memoir 组织前,统一由 `MemoryService` 执行检索编排: - -1. 生成 retrieval query -2. 先做 metadata filter(如 `user_id`、`book_id`、`conversation_id`、`speaker`、`event_year`、`status`、`is_excluded`) -3. 并行执行 FTS 检索、向量检索、摘要检索 -4. 做 score fusion,必要时走可选 reranker -5. 按 token budget 组装 evidence bundle -6. 将结构化上下文交给 `conversation` / `memoir` - -传给 LLM 的上下文不建议直接拼成长文本,而是建议显式区分: - -- `relevant_memories` -- `relevant_summaries` -- `relevant_facts` -- 可选的 `timeline_hints` - -这样更利于控制来源、分数、时间线信息和引用追踪,减少模型遗漏、编造与上下文漂移。 - -### 6.6 依赖方向与端口设计 - -依赖边界如下: - -- `conversation` 只调用 `MemoryService` -- `memoir` 只调用 `MemoryService` -- `memory/service.py` 可依赖 `EmbeddingProvider`、可选 `Reranker`、以及既有 `LLMProvider` -- `memory/repo.py` 只做数据访问与检索查询,不承载生成逻辑 -- `agents/` 不直接读写 memory 表 - -新增 ports 建议: - -- `ports/embedding.py`:必须新增,提供 `embed_text()` / `embed_texts()` -- `ports/reranker.py`:可选新增,留给二期 cross-encoder rerank - -### 6.7 事务与异步任务边界 - -`memory` 遵循既有事务原则:repo 不提交,service 管事务边界。但 `ingest_transcript()` 允许分阶段提交,避免单事务承载向量生成与抽取全链路。 - -以下操作建议放到 `tasks/ + Celery` 异步执行: - -- embedding 生成 -- session / rolling summary 生成 -- fact extraction -- timeline build -- reindex / backfill - -`conversation` 实时主链路只负责触发 ingest 或请求 retrieval,不等待全部 enrich 完成。 - ---- - -## 七、Provider-Agnostic Ports 设计 - -### 7.1 核心可移植契约(Core Portable Contract) - -每个 port 定义为 `typing.Protocol`,仅包含业务真正依赖的最小稳定能力: - -**SMS** (`ports/sms.py`): - -- `send_verification_code(phone: str, code: str) -> bool` - -**LLM** (`ports/llm.py`): - -- `complete(messages: list[Message], temperature: float, ...) -> str` -- `stream(messages: list[Message], ...) -> AsyncIterator[str]` - -**ImageGen** (`ports/image_gen.py`): - -- `generate(prompt: str, size: ImageSize, ...) -> ImageResult` -- `check_status(task_id: str) -> TaskStatus` - -**Storage** (`ports/storage.py`): - -- `upload(key: str, data: bytes, content_type: str) -> str` -- `get_url(key: str, expires: int) -> str` -- `delete(key: str) -> None` - -**TTS** (`ports/tts.py`): - -- `synthesize(text: str, voice: str) -> bytes` - -**ASR** (`ports/asr.py`): - -- `transcribe(audio: bytes, format: str) -> str` - -**Embedding** (`ports/embedding.py`): - -- `embed_text(text: str) -> list[float]` -- `embed_texts(texts: list[str]) -> list[list[float]]` - -**Reranker(可选)** (`ports/reranker.py`): - -- `rerank(query: str, documents: list[str]) -> list[float]` - -### 7.2 Provider Extensions 规则 - -**绝对禁止 service 直接拿 adapter 扩展方法。** 这会打穿 port 边界,让 service 再次耦合到具体 provider。 - -处理厂商特有能力的正确方式: - -- **方案 A(首选):扩充现有 port。** 如果能力具备跨 provider 通用性(如 image_gen 的 inpaint),在 port protocol 上新增方法,各 adapter 按能力实现或抛 `NotImplementedError`。 -- **方案 B:定义第二个 port。** 如果能力确实只属于某个 provider 的特殊功能(如 Liblib 的模板管理),定义一个独立的窄 port(如 `ImageTemplateManager`),只在需要的 feature deps 中注入。service 依赖的仍然是 port protocol,而非 adapter 类。 -- **方案 C:adapter 内部自行消化。** 厂商特有的配置项(如 Liblib template UUID、COS 生命周期策略)作为 adapter 构造参数传入,不暴露给 service。adapter 在实现 port 方法时自行使用这些配置。 - -**判断标准**:如果 service 代码中出现了 `from adapters.xxx import ...`,说明边界被打穿了。 - -### 7.3 DI 装配 - -在 `core/dependencies.py` 中根据 `config.py` 的配置选择 adapter: - -```python -def get_sms_sender() -> SmsSender: - if settings.sms_provider == "tencent": - return TencentSmsSender(settings.tencent_sms) - raise ValueError(f"Unknown SMS provider: {settings.sms_provider}") -``` - -feature 的 `deps.py` 或 router 通过 `Depends(get_sms_sender)` 注入。 - ---- - -## 八、Alembic 集成与 Schema 变更治理 - -### 8.1 初始化 - -- 在 `api/` 根目录执行 `alembic init alembic` -- 修改 `alembic/env.py`,导入所有 feature 的 `models.py` 到统一 `Base.metadata` -- `alembic.ini` 中 `sqlalchemy.url` 读取 `DATABASE_URL` - -### 8.2 初始基线(不依赖 autogenerate) - -当前 schema 来源是 `Base.metadata.create_all()` + 9 个手写 SQL 迁移文件,两者可能已经出现漂移。直接 autogenerate 大概率产出不准确的 diff。 - -**正确步骤**: - -1. 在 staging/dev 环境连接真实数据库,用 `pg_dump --schema-only` 导出当前实际 schema 作为基准参照。 -2. 手写一个空的 Alembic revision 作为 baseline:`alembic revision -m "baseline_empty"`,upgrade/downgrade 均为 pass。 -3. 对现有数据库执行 `alembic stamp head`,将当前库标记为已在 head。 -4. **此后**才开始用 autogenerate 生成增量迁移,每次 review 时对照 `pg_dump` 确认无遗漏。 -5. 将 `migrations/` 目录下的 9 个手写 SQL 归档到 `migrations_legacy/`,不再使用。 - -### 8.3 变更治理规则 - -- 所有 schema 变更必须通过 Alembic migration 合入,禁止直接改 DB -- `autogenerate` 只做草稿,PR 中必须人工 review -- 破坏性变更(删列、改类型)采用 expand/contract:先加新列 -> 双写 -> 迁移数据 -> 切读路径 -> 删旧列 -- data migration 和 schema migration 分开写 -- 每次部署前校验 `alembic current` == `alembic heads` -- `main.py` 移除 `Base.metadata.create_all()`,改为部署流程执行 `alembic upgrade head` - -### 8.4 Model 拆分 - -将 `database/models.py` 的 9 个模型拆入对应 feature: - -- `User` -> `features/user/models.py` -- `RefreshToken`, `SmsVerificationCode` -> `features/auth/models.py`(identity/session 资产,auth 是唯一强依赖方) -- `Conversation`, `Segment` -> `features/conversation/models.py` -- `Book`, `Chapter`, `MemoirState` -> `features/memoir/models.py` -- `Order` -> `features/payment/models.py` - -所有模型共享同一个 `Base = declarative_base()`(放 `core/db.py`),Alembic `env.py` 统一 import 所有 feature models。 - -**跨 feature model 引用规则**:auth/models.py 中的 `RefreshToken.user_id` 外键指向 `users.id`(表名级引用),不需要 import User 类。如需 ORM relationship,通过字符串引用 `relationship("User", ...)`。 - ---- - -## 九、日志统一化(loguru) - -### 9.1 配置 - -在 `core/logging.py` 中: - -- 移除标准库 `logging.basicConfig` -- 配置 loguru:JSON 格式输出、按级别分文件、rotation/retention -- 拦截标准库 logging(`InterceptHandler`),使 uvicorn / sqlalchemy / celery 日志统一走 loguru - -### 9.2 Request Logging Middleware - -新增 middleware,每个请求注入 `request_id`(UUID),通过 loguru 的 `contextualize` 绑定到所有日志: - -```python -with logger.contextualize(request_id=request_id, user_id=user_id): - response = await call_next(request) -``` - -### 9.3 Provider 调用日志 - -在每个 adapter 中统一记录:provider 名称、操作、耗时、成功/失败、错误分类。 - ---- - -## 十、全局异常处理 - -在 `core/errors.py` 中定义异常体系: - -```python -class AppError(Exception): - status_code: int - error_code: str - message: str - -class NotFoundError(AppError): ... -class AuthenticationError(AppError): ... -class ProviderError(AppError): ... -``` - -在 `main.py` 注册全局 exception handler,统一返回格式: - -```json -{"error_code": "PROVIDER_ERROR", "message": "...", "request_id": "..."} -``` - ---- - -## 十一、统一配置 - -将散落的 `os.getenv()` 收归 `core/config.py`,使用 pydantic-settings `BaseSettings`: - -```python -class Settings(BaseSettings): - database_url: str - redis_url: str - secret_key: str - sms_provider: str = "tencent" - storage_provider: str = "tencent_cos" - image_gen_provider: str = "liblib" - # 嵌套配置 - tencent_sms: TencentSmsConfig - tencent_cos: TencentCosConfig - ... - - model_config = SettingsConfigDict(env_file=".env", env_nested_delimiter="__") -``` - -**强约束**:环境变量统一通过 `core/config.py` 中的 `Settings` 单点读取。除配置模块外,禁止在业务代码中直接调用 `load_dotenv()`、`dotenv_values()` 或散落 `os.getenv()`;其他模块只允许 import `settings` 使用配置值。 - -**依赖收敛约定**:Redis 客户端统一使用 `redis` / `redis.asyncio`;JWT 统一使用 `PyJWT`。`api/pyproject.toml` 与 `api/uv.lock` 是依赖真相源,`requirements.txt` 如保留仅作为兼容导出产物,不再作为主入口。 - ---- - -## 十二、迁移执行策略 - -### Phase 0 - 基础设施搭建(无功能变更) - -1. **迁移到 uv**:以 `api/pyproject.toml` 和 `api/uv.lock` 作为依赖真相源;将 `requirements.txt` 的依赖迁入 `pyproject.toml` 的 `[project.dependencies]`。完成迁移后,`requirements.txt` 如继续保留,仅作为兼容导出文件,不再作为主入口 -2. 创建 `app/core/` 骨架:config, db, logging, errors, security, dependencies, middleware, openapi -3. 重写 `get_async_db()` 去掉自动 commit,定义 `get_sync_db()` context manager 供 Celery 使用 -4. 初始化 Alembic:手写空 baseline revision + `stamp head`(不用 autogenerate) -5. 集成 loguru,配置 InterceptHandler 拦截标准库 logging -6. 添加 request_id middleware -7. 定义全局异常体系(仅统一错误响应格式) -8. 建立 OpenAPI 基线:统一 router metadata 规范、共享错误响应、`custom_openapi()` 全局增强、内部接口可见性策略 -9. 预留 memory 基础设施:确认 `pgvector` 扩展、FTS 列与索引策略、`memory` 表族命名与 Alembic 迁移约定 - -**验证点**:`uv sync --dev` 能基于 `api/pyproject.toml` 和 `api/uv.lock` 安装全部依赖,应用能以新 core 启动,所有现有 API 不受影响。注意 `get_async_db()` 的 commit 语义变更需要同步修改所有现有 router 中缺少显式 commit 的写操作。 - -### Phase 1 - Ports 定义 + Adapters 迁移 - -1. 定义 7 个核心 port protocol(sms, llm, image_gen, storage, tts, asr, embedding) -2. 将现有 SDK 代码封装为 adapter,实现对应 protocol -3. 预留可选 `reranker` port(二期再接具体实现) -4. 在 `core/dependencies.py` 中注册 DI factory(含 `get_embedding_provider()`) -5. payment adapter 从 `payment/` 直接迁移(已有好的抽象) - -**验证点**:所有 adapter 通过 port protocol 类型检查,现有功能不变。 - -### Phase 2 - Feature 模块重构(逐个 feature) - -按依赖关系从外到内逐个迁移,建议顺序: - -1. `content`(faq, legal, home)— 最简单,验证 feature 骨架 -2. `plan` — 轻量,少依赖 -3. `quota` — 提升为独立 feature,定义 `QuotaService` -4. `user` — 拆出 User model -5. `auth` — 拆出 RefreshToken/SmsVerificationCode models,依赖 user models + sms port -6. `memory` — 新增独立 feature,打通 source/chunk/summary/fact/timeline 与 hybrid retrieval -7. `payment` — 迁移现有 payment/ 模块 -8. `memoir` — 合并 books/chapters/memoir_state,迁移 processor,并接入 `MemoryService` -9. `conversation` — 最复杂,包含 WebSocket 深度拆分(见下),并接入 transcript ingest / retrieval - -**每个 feature 迁移步骤**: - -- 创建 `models.py`(从全局 models.py 拆出) -- 创建 `repo.py`(从 router 中提取 DB 操作,禁止 commit) -- 创建 `service.py`(从 router 中提取业务逻辑,负责事务提交) -- 创建 `schemas.py`(收集 Pydantic schema) -- 精简 `router.py`(只保留 HTTP 边界:参数解析、响应状态码、OpenAPI 契约声明、调用 service) -- 为每个公开接口补齐 `response_model` / `status_code` / `responses` / `summary` / `description` / `tags` -- 更新 `main.py` 的 `include_router` - -**memory feature 额外要求**: - -- 建立 `memory_sources` / `memory_chunks` / `memory_summaries` / `memory_facts` / `timeline_events` / `memory_curation_actions` -- `memory_chunks` 同时具备 `embedding` 和 `content_tsv` -- 先完成 `MemoryService` 内部接口,再由 `memoir` / `conversation` 接入 -- 后台编辑 API 保留为后续扩展,不阻塞一期基础设施落地 - -**conversation WebSocket 拆分详细步骤**: - -当前 `routers/websocket.py`(1067 行)需拆为 `features/conversation/ws/` 子包: - -- `message_types.py`:提取 `MessageType` 枚举(当前 line 32-45)+ 消息构造工具函数 -- `connection_manager.py`:提取 `ConnectionManager` 类(当前 line 49-120),只保留连接注册/注销/发送,移除 agent/runner 实例持有 -- `profile_collector.py`:提取 `_get_missing_profile_fields`、`_get_filled_profile_fields`、`_apply_extracted_profile`(当前 line 854-898)和资料收集模式分支(line 917-952) -- `quota_guard.py`:封装配额校验逻辑(当前 line 627-637),调用 `quota/service.py` -- `pipeline.py`:核心消息处理管道,编排 ASR -> 落库 -> Agent -> TTS -> 响应,依赖上述模块 + ports(asr, tts, llm) -- `router.py`:WebSocket endpoint,仅做连接生命周期管理(accept, 主循环, disconnect),将每条消息委托给 pipeline -- `protocol.md`:补充消息协议、鉴权、错误码、状态流转、重连与幂等等非 HTTP 契约 - -**验证点**:每迁移一个 feature,运行测试确保该 feature API 行为不变。 - -### Phase 3 - 清理与加固 - -1. 删除旧 `routers/`, `services/`, `database/models.py`, `middleware/`, `migrations/` -2. 删除 `main.py` 中的 `init_db()` / `create_all()` -3. 更新 Dockerfile:改用 uv 安装依赖(`COPY pyproject.toml uv.lock ./` + `uv sync --frozen --no-dev`),或用 `uv export --no-dev > requirements.txt` 导出后继续用 pip -4. 更新 docker-compose、deploy.sh 中的路径和启动命令 -5. 更新 Celery worker 启动命令(`uv run celery ...`) -6. 更新所有测试 import 路径 -7. 补充缺失的测试 - ---- - -## 十三、硬性规则(写入项目规范) - -1. `**common/` / `utils/` / `shared/` / `helpers/` 禁止创建**:只有 truly cross-cutting 能力进 `core/`,业务 DTO/校验/错误码归 feature -2. **router 是 HTTP + OpenAPI 契约边界**:所有数据访问通过 repo,所有业务逻辑通过 service;但 `response_model`、`status_code`、`responses`、`summary`、`description`、`tags` 等契约元数据必须定义在 router / `APIRouter` 上 -3. **service 禁止 import adapter**:只依赖 port protocol,通过 DI 注入。代码中出现 `from adapters.xxx import ...` 即为违规 -4. **feature 间禁止 import router**:跨 feature 调用通过 service 注入 -5. **所有 schema 变更走 Alembic**:禁止直接 DDL -6. **新增 provider 必须实现 port protocol**:不得在 feature 中直接调用 SDK -7. **事务边界:repo 不提交,service 管事务**:`repo.py` 只做 `add/delete/query`,`commit/rollback` 由 service 或 UoW 统一执行。`get_async_db()` 不自动 commit;`flush()` 仅在明确需要提前拿主键或触发约束检查时受控使用 -8. **port 边界不可打穿**:service 需要厂商增强能力时,必须扩充 port 或定义第二个窄 port,禁止直接引用 adapter 扩展方法 -9. **quota 是独立 feature**:conversation、memoir、payment 如需配额检查,通过注入 `QuotaService`,不得直接 import quota 内部函数 -10. **成功响应不强制包装**:`core/errors.py` 只统一错误响应格式 `{error_code, message, request_id}`,成功响应直接返回 Pydantic model / FileResponse / 原始结构 -11. **配置统一通过 `pydantic-settings` 管理**:只允许在 `core/config.py` 中定义 `BaseSettings` / `SettingsConfigDict`;除配置模块外,禁止业务代码直接 `load_dotenv()`、`dotenv_values()` 或散落 `os.getenv()` -12. **依赖真相源是 `api/pyproject.toml` + `api/uv.lock`**:`requirements.txt` 如存在仅作兼容导出,不作为主依赖入口 -13. **运行时基础库保持收敛**:Redis 客户端统一使用 `redis` / `redis.asyncio`,JWT 统一使用 `PyJWT` -14. **service 装配走 FastAPI dependency graph**:router 只依赖 `get_xxx_service()`,不得在路由函数里手工 new service 或自行拼装 `db + adapter + settings` -15. **每个 feature router 必须维护共享 tags / responses**:使用 `APIRouter(prefix=..., tags=..., responses=...)` 收敛文档边界与共享错误响应 -16. **内部接口显式隐藏**:不对外暴露的 path operation 使用 `include_in_schema=False`,禁止依赖“没人知道 URL” -17. **OpenAPI 全局自定义只做增强**:`custom_openapi()` 只做全局 schema 增强,不替代 router 上的正常声明 -18. **WebSocket 协议必须独立成文**:实时消息 schema、错误、鉴权、状态流转、重连与幂等规则不得只存在于代码实现中 -19. **memory 的定义是素材检索与事实组织,不是聊天 persona 记忆**:不实现通用“记住这个”或面向终端用户的自然语言 `forget` -20. **memory 必须采用混合检索**:至少包含 metadata filter + PostgreSQL FTS + vector retrieval;纯向量库不得作为唯一真相源 -21. **原始素材、摘要、事实、时间线必须分层存储**:不得把所有语义压进单一向量表 -22. **memory 写入必须可追溯**:summary / fact / timeline event 必须能追溯到 `source` 或 `chunk` -23. **conversation / memoir 只通过 `MemoryService` 调用 memory**:不得直接查询 memory 表 -24. **memory feature 不得直接耦合具体 embedding SDK**:只依赖 `EmbeddingProvider`(可选 `Reranker` 同理) -25. **所有 pgvector / FTS schema 变更必须走 Alembic**:索引、扩展、generated column、operator class 变更都要纳入 migration -26. **章节生成必须优先使用 evidence bundle**:不得只凭当前会话上下文裸生成 - diff --git a/docs/plans/backend-test-system.md b/docs/plans/backend-test-system.md deleted file mode 100644 index 520d473..0000000 --- a/docs/plans/backend-test-system.md +++ /dev/null @@ -1,577 +0,0 @@ -# Life Echo API Pytest 测试覆盖计划 - -本计划假设架构重构计划已完成,项目已迁移至 `app/` 下的 feature-first 结构。 - -策略:**删除全部旧 unittest,用 pytest 从零重写。** - ---- - -## 一、旧测试处置 - -### 删除(13 个 unittest 文件) - -全部删除。重写时参考其覆盖场景: - -| 旧文件 | 覆盖场景 | 重写到 | -|--------|----------|--------| -| `test_memoir_image_settings.py` | from_env 默认值、无效整数回退 | `unit/adapters/test_image_gen_liblib.py` | -| `test_memoir_image_schema.py` | normalize 无效 status、缺字段 | `unit/features/test_memoir_service.py` | -| `test_memoir_image_parser.py` | 占位符解析、双大括号 | `unit/features/test_memoir_service.py` | -| `test_memoir_image_provider.py` | 签名、提交、轮询、下载 | `unit/adapters/test_image_gen_liblib.py` | -| `test_memoir_image_storage.py` | 单例、上传、下载 URL、错误 | `unit/adapters/test_storage_tencent_cos.py` | -| `test_memoir_image_prompting.py` | 无 LLM 回退、JSON 解析 | `unit/features/test_memoir_service.py` | -| `test_memoir_image_bootstrap.py` | 启用/禁用、动态上限 | `unit/features/test_memoir_service.py` | -| (已删)旧章节补图任务测试 | 已由 `generate_chapter_cover` + `test_chapter_cover_tasks.py` 覆盖章节封面路径 | `tests/test_chapter_cover_tasks.py` | -| `test_process_memoir_segments_image_enqueue.py` | markdown JSON、入队、禁用 | `unit/tasks/test_memoir_tasks.py` | -| `test_memoir_tasks_redis.py` | Redis 锁复用 | `unit/tasks/test_memoir_tasks.py` | -| `test_pdf_service_images.py` | 图片宽高比、签名失败跳过 | `unit/features/test_memoir_service.py` | -| `test_chapters_router_images.py` | 签名 URL、畸形资产 | `unit/features/test_memoir_router.py` | -| `test_websocket_baseline.py` | 无效 token、文本/音频/转写、结束、乱序、去重、重连 | `unit/features/test_ws_pipeline.py` | - -### 移出(2 个手工脚本) - -- `test_sms_verification.py` -> `scripts/manual/sms_verification.py` -- `test_conversation.py` -> `scripts/manual/conversation_e2e.py` - ---- - -## 二、测试目录结构 - -``` -api/ - pyproject.toml # 生产依赖 + dev 依赖 + pytest/coverage 配置 - uv.lock # lockfile,纳入版本控制 - Makefile - - tests/ - conftest.py # unit 层 fixtures - factories.py # make_user(), make_conversation() 等 - fakes/ # port fake 实现(无 __init__.py) - sms.py - llm.py - storage.py - image_gen.py - tts.py - asr.py - helpers.py - - unit/ - core/ - test_config.py - test_security.py - test_errors.py - test_middleware.py - adapters/ - test_sms_tencent.py - test_llm_deepseek.py - test_image_gen_liblib.py - test_storage_tencent_cos.py - test_tts_openai.py - test_asr_whisper.py - test_asr_tencent.py - test_payment_wechat.py - test_payment_alipay.py - features/ - test_auth_service.py - test_auth_router.py - test_conversation_service.py - test_conversation_router.py - test_ws_connection_manager.py - test_ws_message_types.py - test_ws_pipeline.py - test_ws_profile_collector.py - test_ws_quota_guard.py - test_memoir_service.py - test_memoir_router.py - test_payment_service.py - test_payment_router.py - test_user_service.py - test_user_router.py - test_quota_service.py - test_quota_router.py - test_plan_service.py - test_plan_router.py - test_content_router.py - agents/ - test_conversation_agent.py - test_memory_agent.py - tasks/ - test_memoir_tasks.py - - integration/ - conftest.py - test_auth_repo.py - test_conversation_repo.py - test_memoir_repo.py - test_payment_repo.py - test_user_repo.py - test_quota_repo.py - test_auth_service_write.py - test_payment_service_write.py - test_conversation_service_write.py - test_memoir_tasks_db.py - test_auth_flow.py - test_conversation_flow.py - test_payment_flow.py - test_alembic.py -``` - -### 设计原则 - -- **无 `__init__.py`**:测试目录不放 `__init__.py`,避免包导入副作用。 -- **repo 测试全部放 integration/**:不用 SQLite 模拟 Postgres。SQLite 掩盖 JSON/JSONB、约束、排序、方言差异。 -- **router 测试在 unit/**:override service stub,不跑真实 service/repo/DB。 -- **service 测试分两层**:unit mock repo 测编排;integration 真实 Postgres 测关键写路径的事务边界。 -- features 下文件名 flat,减少嵌套。 - ---- - -## 三、基建代码 - -### 3.1 pyproject.toml - -生产依赖通过 `uv add ` 添加到 `[project.dependencies]`,dev 依赖通过 `uv add --dev ` 添加到 `[dependency-groups]`。禁止手动编辑依赖列表。 - -用 `uv sync --dev` 安装全部依赖(含 dev),`uv.lock` 纳入版本控制。不在 `addopts` 中全局过滤 marker。 - -当前开发工具链职责约定: - -- `ruff`:负责 `lint` + `format` -- `pytest`:统一测试入口 -- `pytest-asyncio`:负责 `async def` 测试和异步 fixture -- `pytest-cov`:负责覆盖率统计 -- `pyright`:负责类型检查,但不替代测试和 `lint` - -### 3.2 Makefile - -```makefile -.PHONY: lint format typecheck test test-unit test-integration test-all test-cov - -lint: ## 代码检查 - uv run ruff check . - -format: ## 代码格式化 - uv run ruff format . - -typecheck: ## 类型检查 - uv run pyright - -test: test-unit ## 默认只跑 unit - -test-unit: ## 零外部依赖 - uv run pytest tests/unit/ -x -q - -test-integration: ## 需要 Postgres + Redis - uv run pytest tests/integration/ -x -q - -test-all: ## 全部 - uv run pytest tests/ -x -q - -test-cov: ## 带覆盖率 - uv run pytest tests/ --cov=app --cov-report=term-missing -``` - -### 3.3 tests/conftest.py(unit 测试用) - -关键设计:router 测试 override **service**(不是 port),service 测试直接构造实例注入 fake ports。两者不重叠。 - -```python -import pytest -from unittest.mock import AsyncMock -from httpx import ASGITransport, AsyncClient - - -# ---- Service stubs ---- -# 每个 feature service 一个 AsyncMock,预设 return_value 匹配方法签名。 -# 以 auth 为例,其余 feature 同理。 -@pytest.fixture -def auth_service_stub(): - stub = AsyncMock() - stub.register.return_value = {"access_token": "t", "refresh_token": "r"} - stub.login.return_value = {"access_token": "t", "refresh_token": "r"} - return stub - -@pytest.fixture -def conversation_service_stub(): - return AsyncMock() - -# memoir_service_stub, payment_service_stub, user_service_stub, -# quota_service_stub, plan_service_stub 同理,均为 AsyncMock() - - -# ---- Fake ports(service 层单测用)---- -# 每个 port 一个 fixture,从 tests/fakes/ 导入。 -@pytest.fixture -def fake_sms(): - from tests.fakes.sms import InMemorySmsSender - return InMemorySmsSender() - -@pytest.fixture -def fake_llm(): - from tests.fakes.llm import StubLLMProvider - return StubLLMProvider(default_response="测试回复") - -# fake_storage, fake_image_gen, fake_tts, fake_asr 同理 - - -# ---- Router 测试用 app(override service 层)---- -@pytest.fixture -def app(auth_service_stub, conversation_service_stub, ...): - from app.main import create_app - from app.features.auth.deps import get_auth_service - from app.features.conversation.deps import get_conversation_service - # ... 其余 feature deps - - application = create_app() - application.dependency_overrides[get_auth_service] = lambda: auth_service_stub - application.dependency_overrides[get_conversation_service] = lambda: conversation_service_stub - # ... 其余 feature overrides - return application - -@pytest.fixture -async def client(app): - async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c: - yield c - - -# ---- 认证 fixture ---- -# 必须 override get_current_user 本身,不能只提供 JWT header。 -# get_current_user 通常会查 DB 加载用户,只发 header 会泄漏真实依赖。 -@pytest.fixture -def stub_current_user(): - from tests.factories import make_user - return make_user(id="test-user-id", subscription_type="free") - -@pytest.fixture -def authenticated_app(app, stub_current_user): - from app.core.dependencies import get_current_user - app.dependency_overrides[get_current_user] = lambda: stub_current_user - return app - -@pytest.fixture -async def auth_client(authenticated_app): - async with AsyncClient( - transport=ASGITransport(app=authenticated_app), - base_url="http://test", - ) as c: - yield c -``` - -### 3.4 integration/conftest.py(savepoint 隔离) - -```python -import pytest -from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession -from sqlalchemy import event, create_engine -from app.core.db import Base - -@pytest.fixture(scope="session") -async def pg_engine(): - import os - url = os.environ["TEST_DATABASE_URL"] - engine = create_async_engine(url) - - from alembic.config import Config - from alembic.command import upgrade - cfg = Config("alembic.ini") - cfg.set_main_option("sqlalchemy.url", url.replace("+asyncpg", "+psycopg")) - upgrade(cfg, "head") - - yield engine - await engine.dispose() - -@pytest.fixture -async def pg_session(pg_engine): - """ - 连接级事务 + savepoint。 - service 内部 commit() 变为 savepoint release, - 测试结束时外层事务 rollback 撤销一切。 - - 硬性前提:被测代码必须复用此 session,不得自行创建 session。 - 自行创建 session 的代码(如 Celery 任务)不走此 fixture。 - """ - conn = await pg_engine.connect() - txn = await conn.begin() - session = AsyncSession(bind=conn) - - nested = await conn.begin_nested() - - @event.listens_for(session.sync_session, "after_transaction_end") - def restart_savepoint(s, transaction): - nonlocal nested - if transaction.nested and not transaction._parent.nested: - nested = conn.sync_connection.begin_nested() - - yield session - - await session.close() - await txn.rollback() - await conn.close() - -# ---- Celery 任务用同步 session(不走 savepoint,用 truncate 清理)---- -@pytest.fixture -def sync_session(pg_engine): - import os - sync_url = os.environ["TEST_DATABASE_URL"].replace("+asyncpg", "+psycopg") - sync_engine = create_engine(sync_url) - from sqlalchemy.orm import Session - with Session(sync_engine) as session: - yield session - with sync_engine.begin() as conn: - for table in reversed(Base.metadata.sorted_tables): - conn.execute(table.delete()) - -# ---- Integration app(注入 pg_session + fake ports)---- -@pytest.fixture -async def integration_app(pg_session, fake_sms, fake_llm, fake_storage, - fake_image_gen, fake_tts, fake_asr): - from app.main import create_app - from app.core.db import get_async_db - from app.core.dependencies import ( - get_sms_sender, get_llm_provider, get_storage, - get_image_generator, get_tts_provider, get_asr_provider, - ) - application = create_app() - application.dependency_overrides[get_async_db] = lambda: pg_session - application.dependency_overrides[get_sms_sender] = lambda: fake_sms - application.dependency_overrides[get_llm_provider] = lambda: fake_llm - application.dependency_overrides[get_storage] = lambda: fake_storage - application.dependency_overrides[get_image_generator] = lambda: fake_image_gen - application.dependency_overrides[get_tts_provider] = lambda: fake_tts - application.dependency_overrides[get_asr_provider] = lambda: fake_asr - yield application - application.dependency_overrides.clear() - -@pytest.fixture -async def integration_client(integration_app): - async with AsyncClient( - transport=ASGITransport(app=integration_app), - base_url="http://test", - ) as c: - yield c -``` - -### 3.5 factories.py - -不使用 factory-boy(`SQLAlchemyModelFactory` 默认为同步 Session,和 AsyncSession 配合需大量封装)。用简单同步 helper 函数: - -```python -import uuid -from datetime import datetime, timezone -from app.features.user.models import User -from app.features.conversation.models import Conversation - -_seq = 0 - -def make_user(**overrides) -> User: - global _seq; _seq += 1 - defaults = dict( - id=str(uuid.uuid4()), - phone=f"1380000{_seq:04d}", - nickname=f"用户{_seq}", - subscription_type="free", - created_at=datetime.now(timezone.utc), - ) - defaults.update(overrides) - return User(**defaults) - -# make_conversation, make_segment, make_book, make_chapter, -# make_memoir_state, make_order, make_refresh_token, make_sms_code 同理。 -# 同步构造 model 实例,integration 测试中 session.add() + await session.flush() 入库。 -``` - -### 3.6 Fake Port 实现 - -`tests/fakes/` 下每个 port 一个文件,无 `__init__.py`,实现完整 Protocol 签名: - -- **sms.py** -- `InMemorySmsSender`:`sent: list` 记录历史,`should_fail` 可切换失败模式 -- **llm.py** -- `StubLLMProvider`:`default_response`,`responses: list`(按序消耗),`call_count` -- **storage.py** -- `InMemoryStorage`:`dict[str, bytes]` 后端 -- **image_gen.py** -- `StubImageGenerator`:`generate()` 返回预设结果,`check_status()` 按调用次数切换 -- **tts.py** -- `StubTTSProvider`:返回固定 bytes -- **asr.py** -- `StubASRProvider`:`default_text`,空串模拟转写失败 - ---- - -## 四、分层测试策略 - -### 4.1 Service unit 测试(unit/features/test_*_service.py) - -mock repo + fake ports,测编排逻辑和错误分支。每个 feature 预计 10-25 case。 - -```python -# 示例:展示 service unit test 的边界——mock repo,不碰 DB -async def test_auth_register_duplicate_phone(fake_sms): - repo = AsyncMock() - repo.find_by_phone.return_value = make_user(phone="13800000001") - - service = AuthService(repo=repo, sms=fake_sms) - with pytest.raises(ConflictError): - await service.register(RegisterRequest(phone="13800000001", ...)) -``` - -### 4.2 Service integration 测试(integration/test_*_service_write.py) - -真实 Postgres(savepoint 隔离)+ 真实 repo + fake ports。**只覆盖关键写路径**的事务边界和 ORM 状态持久化: - -- **auth register**:User + RefreshToken 在同一事务创建,失败时两者都不残留 -- **payment callback**:订单状态变更 + 用户订阅升级在同一事务 -- **conversation end**:段落状态标记 + Celery 任务提交边界 - -```python -# 示例:展示与 unit test 的关键区别——验证 DB 中真实存在数据 -@pytest.mark.integration -async def test_register_creates_user_and_token_atomically(pg_session, fake_sms): - repo = AuthRepo(pg_session) - service = AuthService(repo=repo, sms=fake_sms) - - result = await service.register(RegisterRequest(phone="13800000001", ...)) - - user = await pg_session.get(User, result.user_id) - assert user is not None - assert user.phone == "13800000001" - - tokens = await repo.find_refresh_tokens_by_user(user.id) - assert len(tokens) == 1 -``` - -每个 feature 写路径预计 3-8 case,总计约 15-25 case。 - -### 4.3 Router unit 测试(unit/features/test_*_router.py) - -override service stub + get_current_user,只验证 HTTP 契约。与 service 测试不重叠:router 不验证"重复手机号返回什么",只验证"service 抛 ConflictError 时 router 返回 409"。 - -```python -# 示例:展示 router test 的边界——验证 HTTP 契约,不碰 service 内部 -async def test_register_calls_service(client, auth_service_stub): - resp = await client.post("/api/auth/register", json={"phone": "13800000001", ...}) - assert resp.status_code == 201 - auth_service_stub.register.assert_called_once() - -async def test_profile_requires_auth(client): - resp = await client.get("/api/user/profile") - assert resp.status_code == 401 -``` - -每个 feature 预计 8-20 case。 - -### 4.4 Repo integration 测试(integration/test_*_repo.py) - -真实 Postgres(savepoint 隔离),factory helper 构造数据。不用 SQLite。 - -```python -# 示例:展示 repo test 的边界——真实 Postgres,验证 SQL 正确性 -@pytest.mark.integration -async def test_find_refresh_token(pg_session): - user = make_user() - pg_session.add(user) - token = make_refresh_token(user_id=user.id, token="abc123") - pg_session.add(token) - await pg_session.flush() - - repo = AuthRepo(pg_session) - found = await repo.find_refresh_token("abc123") - assert found is not None - assert found.user_id == user.id -``` - -每个 feature 预计 5-15 case。 - -### 4.5 Adapter unit 测试(unit/adapters/) - -`respx` mock httpx / `unittest.mock.patch` mock SDK client。每个 adapter 至少:成功路径 + 网络超时 + rate limit + 响应格式异常。每个 adapter 预计 8-15 case。 - -### 4.6 WebSocket unit 测试(unit/features/test_ws_*.py) - -5 个文件对应 ws/ 子包 5 个模块: - -- **test_ws_connection_manager.py**:注册/注销、并发同 ID、断开容错(6-8 case) -- **test_ws_message_types.py**:枚举完整性、序列化(5-6 case) -- **test_ws_pipeline.py**:文本->Agent->落库、音频->ASR->Agent->TTS、无效 token、ASR 失败降级、乱序聚合、幂等去重、结束->整理、重连(15-20 case) -- **test_ws_profile_collector.py**:缺失检测、提取、已满跳过(5-8 case) -- **test_ws_quota_guard.py**:配额内通过、耗尽拒绝、plan 差异(5-8 case) - -### 4.7 Agents unit 测试 - -注入 StubLLMProvider,验证 prompt 构建、响应解析、异常降级。每个 agent 8-12 case。 - -### 4.8 Celery Tasks 测试——两层 - -**Unit**(unit/tasks/test_memoir_tasks.py):mock repo/service 测编排逻辑。20-25 case。 - -**Integration**(integration/test_memoir_tasks_db.py):真实 Postgres + fake ports,验证 DB 状态机持久化。纯 mock 测不到 Chapter 状态转换、segment is_processed 标记、image_assets JSON 持久化。 - -由于 Celery 任务使用同步 Session(不经过 DI),savepoint 隔离不住,改用 truncate 策略(见 `integration/conftest.py` 中的 `sync_session` fixture)。5-10 case,只覆盖关键状态转换。 - ---- - -## 五、端到端流程 + Alembic - -### 端到端流程(integration/) - -- **test_auth_flow.py**:注册 -> 登录 -> 获取 profile -> 刷新 -> 登出 -> 旧 token 失效 -- **test_conversation_flow.py**:创建对话 -> 发消息 -> Agent 回复 -> 消息列表 -> 结束 -> 触发整理 -- **test_payment_flow.py**:查看计划 -> 创建订单 -> 模拟回调 -> 验证订阅类型变更 - -### Alembic 测试 - -Forward-only smoke,不做 downgrade(业务 migration 不一定可逆): -- `test_alembic_upgrade_clean_db`:空库 upgrade head 无报错 -- `test_alembic_current_at_head`:验证测试库 current == head - ---- - -## 六、CI 集成 - -```yaml -jobs: - test: - name: Run Tests - runs-on: ubuntu-latest - services: - postgres: - image: postgres:16 - env: { POSTGRES_DB: life_echo_test, POSTGRES_PASSWORD: test } - ports: ["5432:5432"] - options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 - redis: - image: redis:7 - ports: ["6379:6379"] - defaults: - run: - working-directory: api - steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v4 - - run: uv sync --dev - - name: Unit tests - run: make test-unit - - name: Integration tests - run: make test-integration - env: - TEST_DATABASE_URL: postgresql+asyncpg://postgres:test@localhost:5432/life_echo_test - REDIS_URL: redis://localhost:6379/0 - - uses: codecov/codecov-action@v4 - - build-and-push: - needs: test - # ... 现有 build 步骤 ... -``` - ---- - -## 七、测试规则 - -1. **新代码必须附带测试**,PR 不通过无测试的业务变更 -2. **全部使用 pytest 函数 + fixture 风格** -3. **repo 测试只在 integration/ 中跑 Postgres**,不用 SQLite -4. **service unit 测试 mock repo + fake ports**;关键写路径补 service integration 测试(真实 Postgres + fake ports) -5. **router 测试 override service stub + get_current_user**,只验证 HTTP 契约 -6. **adapter 测试 mock 外部调用**,至少成功 + 2 种错误场景 -7. **每个 test function 状态独立**(unit 无 DB;integration 用 savepoint 隔离) -8. **savepoint 硬性前提:被测代码必须复用注入的 session,不得自行创建**。Celery 任务用独立测试库 + truncate -9. **integration 标记 `@pytest.mark.integration`** -10. **默认命令 `make test`**(unit),`make test-all` 全部 -11. **测试目录无 `__init__.py`** -12. **依赖通过 uv 管理**:`uv sync --dev` 安装,`uv add --dev ` 新增 dev 依赖,禁止 `pip install` -13. **类型检查由 `pyright` 负责**:它用于发现静态类型问题,但不能替代 `ruff` 或 `pytest` diff --git a/docs/plans/memory-capability-gap.md b/docs/plans/memory-capability-gap.md deleted file mode 100644 index f1e2632..0000000 --- a/docs/plans/memory-capability-gap.md +++ /dev/null @@ -1,176 +0,0 @@ -# Memory 能力差距文档 - -> 面向回忆录生产的素材检索与事实组织系统 — 当前实现状态与待完成项。 - ---- - -## 一、定位与目标(来自架构计划) - -本项目中的 `memory` 定义为: - -- **面向回忆录生产的可检索素材资产** -- **可持续抽取、修正、确认的结构化事实层** -- **为章节生成和追问生成提供 grounding 的 RAG 基础设施** - -明确不做: - -- 通用「记住这个」的终端用户指令式记忆 -- 聊天助手式 persona memory -- 面向终端用户的自然语言 `forget that` - ---- - -## 二、当前已就绪部分 - -### 2.1 数据模型(`features/memory/models.py`) - -| 表 | 状态 | 说明 | -|----|------|------| -| `memory_sources` | ✅ | 原始素材主记录,含 source_type、raw_text、conversation_id、speaker、captured_at | -| `memory_chunks` | ✅ | 检索单元,含 content、embedding(pgvector)、content_tsv(FTS)、chunk_index、is_excluded | -| `memory_summaries` | ✅ | 会话/滚动/主题摘要,含 source_chunk_ids 追溯 | -| `memory_facts` | ✅ | 候选/已确认事实,含 fact_type、subject、predicate、source_chunk_id | -| `timeline_events` | ✅ | 时间线事件,含 event_year、source_fact_ids | -| `memory_curation_actions` | ✅ | 操作轨迹(exclude/restore/correct/confirm/reject) | - -### 2.2 接口与依赖 - -| 组件 | 状态 | 说明 | -|------|------|------| -| `MemoryService` 类 | ✅ | 门面已定义,`ingest_transcript`、`retrieve` 签名就绪 | -| `MemoirService.get_evidence()` | ✅ | 通过注入 `MemoryService` 调用 retrieve | -| `MemoirService` 注入 `MemoryService` | ✅ | `deps.py` 已配置 | -| `EmbeddingProvider` port | ✅ | OpenAI adapter 已实现 | -| `get_embedding_provider()` | ✅ | `core/dependencies.py` 已注册 | - -### 2.3 骨架文件 - -| 文件 | 状态 | 说明 | -|------|------|------| -| `chunker.py` | 骨架 | `chunk_transcript()` 未实现 | -| `repo.py` | 骨架 | 无具体 CRUD | -| `retriever.py` | 骨架 | `HybridRetriever.retrieve()` 未实现 | -| `extractor.py` | 骨架 | `extract_facts()` 未实现 | -| `summarizer.py` | 骨架 | `generate_session_summary`、`generate_rolling_summary` 未实现 | -| `timeline.py` | 骨架 | 无具体逻辑 | -| `curation.py` | 骨架 | 无具体逻辑 | - ---- - -## 三、待完成能力(按优先级) - -### 3.1 写入路径(Ingest) - -**目标**:对话结束后,将 transcript 沉淀为 memory 资产,并异步补齐 embedding、summary、fact。 - -| 步骤 | 当前状态 | 待实现 | -|------|----------|--------| -| 1. `MemoryService.ingest_transcript()` | `raise NotImplementedError` | 写入 `memory_sources`、切块写入 `memory_chunks`、提交主事务 | -| 2. 触发异步任务 | 无 | Celery 任务:embedding 生成、summary 生成、fact 提取、timeline 构建 | -| 3. `chunker.chunk_transcript()` | `raise NotImplementedError` | 按 max_tokens/overlap 切分,支持 speaker 边界 | -| 4. `memory/repo.py` | 空 | `create_source()`、`create_chunks()`、`update_chunk_embedding()` 等 | -| 5. `memory_tasks.py` | 无 | 新增 `enrich_memory_source` 等 Celery 任务 | - -**接入点**:`conversation/ws/router.py` 中 `END_CONVERSATION` 分支,在 `process_conversation_segments` 之后调用 `MemoryService.ingest_transcript()`。需从 segments 聚合 transcript 文本。 - -### 3.2 读取路径(Retrieve) - -**目标**:混合检索(metadata + FTS + vector)生成 evidence bundle,供 memoir 章节生成使用。 - -| 步骤 | 当前状态 | 待实现 | -|------|----------|--------| -| 1. `HybridRetriever.retrieve()` | `raise NotImplementedError` | metadata filter → FTS 查询 → 向量检索 → score fusion | -| 2. `MemoryService.retrieve()` | 返回空 dict | 调用 `HybridRetriever`,按 token budget 组装 evidence bundle | -| 3. `memory/repo.py` | 空 | `search_chunks_fts()`、`search_chunks_vector()`、`get_summaries()`、`get_facts()` | -| 4. `memory_chunks.content_tsv` | 模型有列 | 需 Alembic 迁移:generated tsvector 列 + GIN index | -| 5. `memory_chunks.embedding` | 模型有列 | 需异步任务写入,pgvector 索引已由 pgvector 扩展支持 | - -**Evidence Bundle 结构**(已定义于 `memory/schemas.py`): - -```python -{ - "relevant_chunks": [...], - "relevant_summaries": [...], - "relevant_facts": [...], - "timeline_hints": [...], -} -``` - -### 3.3 消费端接入 - -**目标**:章节生成必须优先使用 evidence bundle(规则 26)。 - -| 消费端 | 当前状态 | 待实现 | -|--------|----------|--------| -| `process_conversation_segments` | 未调用 ingest | 无(ingest 在 END_CONVERSATION 时由 router 触发) | -| `process_memoir_segments`(Celery) | 未使用 evidence | 在生成章节前调用 `MemoryService.retrieve()` 或通过 MemoirService 传入 | -| `generate_chapter_content`(Celery) | 未使用 evidence | 同上 | -| `MemoirGenerator.generate_narrative` | 仅用 slots + content | 增加 evidence 参数,拼入 prompt | -| `get_narrative_prompt` | 无 evidence 参数 | 增加 `evidence_bundle: dict` 参数,格式化后注入 prompt | - -**注意**:Celery 任务为同步,需通过 `get_sync_db()` 获取 session,并调用 `MemoryService` 的同步版本或封装。当前 `MemoryService` 为 async,需考虑: -- 方案 A:在 Celery 中 `asyncio.run(memory_service.retrieve(...))` -- 方案 B:定义 `MemoryServiceSync` 或 `retrieve_sync` 供 Celery 使用 - -### 3.4 辅助能力(二期可延后) - -| 能力 | 文件 | 说明 | -|------|------|------| -| fact extraction | `extractor.py` | LLM 抽取结构化事实 | -| session summary | `summarizer.py` | 会话摘要、滚动摘要 | -| timeline build | `timeline.py` | 按 event_year 组织时间线 | -| curation | `curation.py` | exclude/restore/correct 操作 | -| reranker | `ports/reranker.py` | 可选 cross-encoder 重排 | - ---- - -## 四、实现顺序建议 - -1. **Phase 1:写入路径** - - 实现 `chunk_transcript()` - - 实现 `memory/repo.py` 的 `create_source`、`create_chunks` - - 实现 `MemoryService.ingest_transcript()` - - 在 END_CONVERSATION 时调用 ingest(需聚合 segments 的 transcript) - -2. **Phase 2:异步补齐** - - 新增 `memory_tasks.py`,Celery 任务:embedding 生成、写入 `memory_chunks.embedding` - - 可选:summary、fact 提取(可后续迭代) - -3. **Phase 3:读取路径** - - 实现 `repo.search_chunks_fts()`、`search_chunks_vector()`(需 FTS 迁移) - - 实现 `HybridRetriever.retrieve()`(metadata + FTS + vector + 融合) - - 实现 `MemoryService.retrieve()` 调用 HybridRetriever - -4. **Phase 4:消费端接入** - - 修改 `get_narrative_prompt` 增加 evidence 参数 - - 修改 `MemoirGenerator.generate_narrative` 接收 evidence - - 修改 `process_memoir_segments`、`generate_chapter_content` 在生成前获取 evidence 并传入 - ---- - -## 五、Alembic 迁移待办 - -| 迁移 | 说明 | -|------|------| -| `memory_chunks` FTS | 需 `content_tsv` 使用 `tsvector` generated column + GIN index,或应用层维护 | -| 若已有 `create_all` 建表 | 需 `alembic revision --autogenerate` 生成与当前 models 一致的迁移,或手写等效 DDL | - ---- - -## 六、依赖规则 - -- `conversation` / `memoir` 不得直接读写 memory 表,只能调用 `MemoryService` -- `agents/` 不得直接读写 memory 表,只能消费 evidence bundle -- `memory` 不得直接 import `adapters`,只依赖 `EmbeddingProvider` port - ---- - -## 七、预估工作量 - -| 阶段 | 预估 | 说明 | -|------|------|------| -| Phase 1 写入路径 | 1–1.5 天 | chunker、repo、ingest、END_CONVERSATION 接入 | -| Phase 2 异步补齐 | 0.5–1 天 | Celery 任务、embedding 写入 | -| Phase 3 读取路径 | 1–1.5 天 | FTS、vector、HybridRetriever、MemoryService.retrieve | -| Phase 4 消费端接入 | 1 天 | prompt、MemoirGenerator、Celery 任务 | -| **合计** | **约 3.5–5 人天** | 视测试与联调深度而定 | diff --git a/docs/plans/multi-agent-refactor-plan.md b/docs/plans/multi-agent-refactor-plan.md deleted file mode 100644 index 8e23371..0000000 --- a/docs/plans/multi-agent-refactor-plan.md +++ /dev/null @@ -1,22 +0,0 @@ -# 三模块多 Agent 改造方案(归档) - -> 本文档已归档。早期大段“迁移中”设计会持续引入错误心智模型,因此这里仅保留结果摘要;旧细节请直接查 Git 历史。 - -## 当前正式架构 - -- Chat:唯一实时编排入口是 `ChatOrchestrator`;`ProfileAgent` / `InterviewAgent` 为 specialist。 -- Conversation history:DB `conversation_messages` 为真源,Redis 仅作缓存。 -- Memoir:`MemoirOrchestrator.prepare_batches` 负责分桶;`run_story_pipeline_for_category_batch` 负责 story 写入与 chapter 物化。 -- Images:正文主图走 `generate_story_image`;章节封面走 `try_enqueue_generate_chapter_cover` -> `generate_chapter_cover`。 - -## 已删除路径 - -- 旧 chat facade -- 旧 memoir facade -- agent-layer runner -- 旧章节补图任务名 - -## 约束 - -- 不再为旧入口保留 facade、alias 或兼容任务名。 -- 新设计、测试、代码 review 一律以上述正式路径为准。 diff --git a/docs/react-web-migration.md b/docs/react-web-migration.md deleted file mode 100644 index 172b753..0000000 --- a/docs/react-web-migration.md +++ /dev/null @@ -1,225 +0,0 @@ -# app-android 迁移至 React Web 应用计划 - -> **目标**:将现有 Kotlin + Jetpack Compose 原生 Android 应用迁移为**纯 React Web 应用**,不再依赖任何原生 iOS/Android 代码,统一为浏览器端运行。 - ---- - -## 1. app-android 现状摘要 - -### 1.1 技术栈 - -| 层级 | 技术 | 说明 | -|------|------|------| -| 语言 | Kotlin 1.9+ | 主语言 | -| UI | Jetpack Compose | 声明式 UI | -| 架构 | MVVM | ViewModel + Compose | -| 网络 | Ktor | HTTP + WebSocket | -| 本地存储 | Room + DataStore | SQLite 数据库 + 键值存储 | -| 导航 | Navigation Compose | 声明式导航 | -| 图片 | Coil | 图片加载 | -| 主题 | Material 3 | 深色/浅色主题 | -| 支付 | 微信支付 / 支付宝 | 原生 SDK 集成 | - -### 1.2 项目结构 - -``` -app-android/ -├── config/ # AppConfig (API 地址、BuildConfig) -├── data/ # 数据层 -│ ├── auth/ # TokenManager (DataStore) -│ ├── database/ # Room (AppDatabase, Entity, DAO) -│ ├── preferences/ # TokenPreferences -│ └── repository/ # Conversation/Chapter/Message/Payment/Profile Repository -├── network/ # 网络层 -│ ├── ApiService # REST API (Ktor + Bearer Auth) -│ ├── AuthService # 登录/注册/刷新 Token -│ ├── WebSocketClient -│ └── models/ # 请求/响应 DTO -├── ui/ -│ ├── screens/ # 20+ 屏幕 -│ ├── components/ # 可复用组件 -│ ├── theme/ # Color, Theme, Type -│ ├── viewmodel/ # 7 个 ViewModel -│ └── settings/ # AppSettings (深色模式等) -├── navigation/ # Screen 路由 + AppNavigation -├── feature/ # VoiceRecorder 等 -└── utils/ # 工具类 -``` - -### 1.3 功能模块清单 - -| 模块 | screens / 组件 | ViewModel | -|------|-----------------|-----------| -| 认证 | Login, Register, NicknameSetup, ResetPassword, AccountManagement | AuthViewModel | -| 对话 | ConversationList, CreateMemory | ConversationListViewModel, CreateMemoryViewModel | -| 回忆录 | MyMemoir, ChapterReading, FullTextReading | MyMemoirViewModel | -| 个人中心 | Profile, PersonalInfo | ProfileViewModel | -| 套餐/支付 | UpgradePlan, PlanDetails, PlanBalance, MyOrders | PaymentViewModel | -| 设置/帮助 | FAQ, Feedback, About, ExportData, LegalDocument | - | - -### 1.4 核心能力 - -- **认证**:手机号注册、登录、JWT Token 管理、刷新 Token、DataStore 持久化 -- **对话**:WebSocket 实时语音对话、消息收发、TTS 播放 -- **回忆录**:章节列表、阅读、全文、PDF 导出、对话整理为章节 -- **支付**:微信支付、支付宝、套餐、订单、余额 -- **本地数据**:Room 存储 Conversation/Chapter/Message,离线同步 -- **主题**:深色模式、Material 3 - ---- - -## 2. 迁移策略 - -- 新建 **React Web 项目**(Vite + React 或 Create React App),不复用 app-ios -- 按 **模块/功能** 逐步迁移,优先核心流程(登录 → 对话 → 回忆录) -- 保留 API 契约不变,仅替换客户端实现 -- **零原生代码**:纯 Web 技术栈,部署为静态站点或 SPA - ---- - -## 3. 迁移步骤 TODO - -### Phase 0:环境与基建(前置) - -| # | 任务 | 说明 | 优先级 | -|---|------|------|--------| -| 0.1 | 初始化 React Web 项目 | `npm create vite@latest app-web -- --template react-ts` 或 CRA | P0 | -| 0.2 | 配置 API 环境 | 将 AppConfig.kt 中 BASE_URL / WS_BASE_URL 迁移为 env (VITE_*) | P0 | -| 0.3 | 搭建项目目录结构 | src/screens, components, services, store, hooks, themes | P1 | -| 0.4 | 配置路由 | React Router v6 | P0 | - -### Phase 1:认证与 Token 管理 - -| # | 任务 | 对应 Android 实现 | 说明 | 优先级 | -|---|------|-------------------|------|--------| -| 1.1 | Auth API 封装 | AuthService.kt | 注册、登录、刷新 Token | P0 | -| 1.2 | Token 持久化 | TokenManager + DataStore | localStorage 或 sessionStorage | P0 | -| 1.3 | 401 自动刷新 + 登出 | ApiService Auth plugin | axios 拦截器实现 Bearer + 刷新 | P0 | -| 1.4 | LoginScreen | LoginScreen.kt | 手机号 + 密码登录 | P0 | -| 1.5 | RegisterScreen | RegisterScreen.kt | 手机号注册 | P0 | -| 1.6 | NicknameSetupScreen | NicknameSetupScreen.kt | 首次登录昵称设置 | P1 | -| 1.7 | ResetPasswordScreen | ResetPasswordScreen.kt | 重置密码 | P1 | -| 1.8 | AccountManagementScreen | AccountManagementScreen.kt | 账号管理、登出 | P1 | - -### Phase 2:导航与主框架 - -| # | 任务 | 对应 Android 实现 | 说明 | 优先级 | -|---|------|-------------------|------|--------| -| 2.1 | 路由定义 | Screen sealed class | 定义所有 route 常量 / 路径 | P0 | -| 2.2 | 底部 Tab 导航 | MainActivity BottomNav | 聊天 / 回忆录 / 我的 三个 Tab | P0 | -| 2.3 | 栈式页面导航 | AppNavigation.kt | React Router `` / 嵌套路由 | P0 | -| 2.4 | 登录态路由守卫 | ProtectedRoute 组件 | 未登录 → 重定向 Login | P0 | -| 2.5 | 过渡动画 | NavigationTransitions | 视需求用 CSS transition 或 Framer Motion | P2 | - -### Phase 3:REST API 与数据层 - -| # | 任务 | 对应 Android 实现 | 说明 | 优先级 | -|---|------|-------------------|------|--------| -| 3.1 | API Client 封装 | ApiService.kt | axios 实例、baseURL、timeout | P0 | -| 3.2 | 对话相关 API | ApiService 对话接口 | create/list/get conversation | P0 | -| 3.3 | 回忆录/章节 API | ApiService 章节/书籍接口 | books, chapters, PDF 等 | P0 | -| 3.4 | 支付/套餐 API | ApiService 支付接口 | plans, orders, balance | P1 | -| 3.5 | 用户/配置 API | ApiService Profile | 用户信息、FAQ、反馈 | P1 | -| 3.6 | 数据模型 (TypeScript) | network/models/*.kt | 定义请求/响应类型 | P0 | - -### Phase 4:WebSocket 与对话功能 - -| # | 任务 | 对应 Android 实现 | 说明 | 优先级 | -|---|------|-------------------|------|--------| -| 4.1 | WebSocket Client | WebSocketClient.kt | 原生 `WebSocket` API,心跳、重连 | P0 | -| 4.2 | WebSocket 消息协议 | WebSocketMessage.kt | type / payload 解析 | P0 | -| 4.3 | 消息收发逻辑 | CreateMemoryViewModel | sendAudio/sendText、接收 TTS/文本 | P0 | -| 4.4 | 浏览器录音 | VoiceRecorder.kt | `MediaRecorder` API(getUserMedia) | P0 | -| 4.5 | ConversationListScreen | ConversationListScreen.kt | 对话列表 | P0 | -| 4.6 | CreateMemoryScreen | CreateMemoryScreen.kt | 聊天详情、语音输入 | P0 | -| 4.7 | MessageBubble / ChatInputField | 对应 Compose 组件 | 消息气泡、输入框 | P0 | -| 4.8 | 音频播放 (TTS) | CreateMemoryViewModel 中 TTS | `HTMLAudioElement` / `new Audio()` | P0 | - -### Phase 5:回忆录模块 - -| # | 任务 | 对应 Android 实现 | 说明 | 优先级 | -|---|------|-------------------|------|--------| -| 5.1 | MyMemoirScreen | MyMemoirScreen.kt | 回忆录 Tab 主页 | P0 | -| 5.2 | 章节列表 | ChapterCard, BookInfoCard | 书籍/章节列表展示 | P0 | -| 5.3 | ChapterReadingView | 章节阅读 | 单章阅读 | P0 | -| 5.4 | FullTextReadingView | 全文阅读 | 完整回忆录阅读 | P1 | -| 5.5 | PDF 导出 | MyMemoirViewModel 导出逻辑 | 调用 API 获取 PDF,`blob` 下载或新窗口打开 | P1 | -| 5.6 | OrganizeConversationDialog | 对话整理为章节 | 弹窗 + API 调用 | P1 | - -### Phase 6:个人中心与支付 - -| # | 任务 | 对应 Android 实现 | 说明 | 优先级 | -|---|------|-------------------|------|--------| -| 6.1 | ProfileScreen | ProfileScreen.kt | 我的 Tab、未登录/已登录 UI | P0 | -| 6.2 | PersonalInfoScreen | PersonalInfoScreen.kt | 个人信息、头像上传 | P1 | -| 6.3 | UpgradePlanScreen | UpgradePlanScreen.kt | 套餐选择 | P1 | -| 6.4 | PlanDetailsScreen / PlanBalanceScreen | 套餐详情、余额 | P1 | -| 6.5 | MyOrdersScreen | 订单列表 | P1 | -| 6.6 | 微信/支付宝支付 | PaymentUtils | 使用 H5 支付:微信 JSAPI / 支付宝网页支付 | P2 | -| 6.7 | FAQScreen / FeedbackScreen | 常见问题、反馈 | P1 | -| 6.8 | AboutScreen / LegalDocumentScreen | 关于、条款、隐私 | P1 | -| 6.9 | ExportDataScreen | 数据导出 | P2 | - -### Phase 7:本地存储(可选) - -| # | 任务 | 对应 Android 实现 | 说明 | 优先级 | -|---|------|-------------------|------|--------| -| 7.1 | 本地缓存 | Room + Repository | localStorage / IndexedDB(如 dexie.js)| P2 | -| 7.2 | 离线同步逻辑 | Repository 同步 | 视产品需求决定是否保留 | P2 | - -### Phase 8:主题、设置与收尾 - -| # | 任务 | 对应 Android 实现 | 说明 | 优先级 | -|---|------|-------------------|------|--------| -| 8.1 | 主题系统 | LifeechoTheme, Color.kt | CSS 变量 + 深色/浅色 media/切换 | P1 | -| 8.2 | 主题持久化 | AppSettings.kt | localStorage 保存用户偏好 | P1 | -| 8.3 | 国际化 (i18n) | strings.xml | react-i18next 或类似 | P2 | -| 8.4 | 麦克风权限 | RECORD_AUDIO | 浏览器 `navigator.mediaDevices.getUserMedia` 需 HTTPS | P0 | - -### Phase 9:构建与发布 - -| # | 任务 | 说明 | 优先级 | -|---|------|------|--------| -| 9.1 | 生产构建 | `npm run build`(Vite 输出 dist/)| P0 | -| 9.2 | 静态托管部署 | 部署至 Vercel / Netlify / Nginx 等 | P0 | -| 9.3 | 版本号与 CI | 对齐现有 GitHub Actions(若有)| P1 | - ---- - -## 4. 技术映射速查 - -| Android (Kotlin) | React Web 对应 | -|------------------|-----------------| -| Jetpack Compose | React 组件 + CSS / Tailwind / styled-components | -| ViewModel + StateFlow | useState / useReducer / Zustand / React Query | -| Navigation Compose | React Router v6 | -| Ktor HttpClient | axios / fetch | -| Ktor WebSocket | 原生 `WebSocket` | -| Room | localStorage / IndexedDB(或仅网络)| -| DataStore | localStorage / sessionStorage | -| Coil | `` / 原生加载 | -| Material 3 | 自定义主题 / MUI / shadcn/ui | - ---- - -## 5. 风险与注意事项 - -1. **WebSocket + 语音**:浏览器端 WebSocket 稳定,录音需 `getUserMedia`,HTTPS 环境下可用。 -2. **支付**:Web 端使用微信 H5 支付、支付宝网页支付,无需原生 SDK。 -3. **本地数据**:Web 无 SQLite,可用 localStorage 简单缓存或 IndexedDB 做离线。 -4. **性能**:长列表用虚拟滚动(react-window / react-virtuoso)。 -5. **PWA**:可选做成 PWA,支持离线与「添加到主屏幕」。 - ---- - -## 6. 建议执行顺序 - -1. **Phase 0 + 1 + 2**:基建 + 认证 + 导航,打通「登录 → 主框架」。 -2. **Phase 3 + 4**:API + WebSocket + 对话,实现核心对话流程。 -3. **Phase 5**:回忆录浏览与阅读。 -4. **Phase 6**:个人中心与基础套餐展示(支付可延后)。 -5. **Phase 7 / 8 / 9**:按需推进本地存储、主题、构建发布。 - ---- - -*文档版本:1.0 | 更新日期:2025-02-15* diff --git a/docs/todo/2026-03-12-app-android-overall-architecture-plan.md b/docs/todo/2026-03-12-app-android-overall-architecture-plan.md deleted file mode 100644 index cd4088f..0000000 --- a/docs/todo/2026-03-12-app-android-overall-architecture-plan.md +++ /dev/null @@ -1,739 +0,0 @@ -# App-Android Overall Architecture Refactor Plan - -**Document Type:** Abstract architecture plan - -**Scope:** `app-android` whole-project refactor - -**Purpose:** Define the target architecture, target file structure, and phased work sequence for the Android client without dropping into code-level implementation details. - -**Primary References:** This plan is based on the Android official guidance set provided for this task, including architecture recommendations, app architecture overview, Android component fundamentals, UI layer, UI events, state holders, state production, View Binding, Data Binding, Lifecycle, Compose integration, ViewModel, saved state, LiveData, coroutines, Paging 3, domain layer, data layer, offline-first, DataStore, App Startup, and modularization. - -## Official Reference Links - -### Architecture Foundations - -- https://developer.android.com/topic/architecture/recommendations -- https://developer.android.com/topic/architecture -- https://developer.android.com/guide/components/fundamentals -- https://developer.android.com/topic/architecture/domain-layer -- https://developer.android.com/topic/architecture/data-layer -- https://developer.android.com/topic/architecture/data-layer/offline-first - -### UI Layer and State - -- https://developer.android.com/topic/architecture/ui-layer -- https://developer.android.com/topic/architecture/ui-layer/events -- https://developer.android.com/topic/architecture/ui-layer/stateholders -- https://developer.android.com/topic/architecture/ui-layer/state-production -- https://developer.android.com/topic/libraries/architecture/compose -- https://developer.android.com/topic/libraries/architecture/saving-states - -### ViewModel and Lifecycle - -- https://developer.android.com/topic/libraries/architecture/lifecycle -- https://developer.android.com/topic/libraries/architecture/viewmodel -- https://developer.android.com/topic/libraries/architecture/viewmodel/viewmodel-factories -- https://developer.android.com/topic/libraries/architecture/viewmodel/viewmodel-apis -- https://developer.android.com/topic/libraries/architecture/viewmodel/viewmodel-savedstate -- https://developer.android.com/topic/libraries/architecture/viewmodel/viewmodel-cheatsheet -- https://developer.android.com/topic/libraries/architecture/livedata -- https://developer.android.com/topic/libraries/architecture/coroutines - -### View Binding and Data Binding - -- https://developer.android.com/topic/libraries/view-binding -- https://developer.android.com/topic/libraries/view-binding/migration -- https://developer.android.com/topic/libraries/data-binding -- https://developer.android.com/topic/libraries/data-binding/start -- https://developer.android.com/topic/libraries/data-binding/expressions -- https://developer.android.com/topic/libraries/data-binding/observability -- https://developer.android.com/topic/libraries/data-binding/generated-binding -- https://developer.android.com/topic/libraries/data-binding/binding-adapters -- https://developer.android.com/topic/libraries/data-binding/architecture -- https://developer.android.com/topic/libraries/data-binding/two-way - -### Data, Persistence, and Startup - -- https://developer.android.com/topic/libraries/architecture/datastore -- https://developer.android.com/topic/libraries/app-startup - -### Paging - -- https://developer.android.com/topic/libraries/architecture/paging/v3-overview -- https://developer.android.com/topic/libraries/architecture/paging/v3-paged-data -- https://developer.android.com/topic/libraries/architecture/paging/v3-network-db -- https://developer.android.com/topic/libraries/architecture/paging/v3-transform -- https://developer.android.com/topic/libraries/architecture/paging/load-state -- https://developer.android.com/topic/libraries/architecture/paging/test -- https://developer.android.com/topic/libraries/architecture/paging/v3-migration - -### Modularization - -- https://developer.android.com/topic/modularization -- https://developer.android.com/topic/modularization/patterns - ---- - -## 1. Overall Architecture Goals - -The Android project should evolve from a single-module application with mixed package responsibilities into a feature-oriented architecture with clear layer boundaries, explicit state ownership, and stable app entry points. - -The target end state is defined by the following goals: - -### 1.1 App Shell Stays Thin - -`app` should only own: - -- Android entry points -- root navigation -- app startup wiring -- dependency assembly -- app-wide configuration boundaries - -`Activity`, `Application`, and manifest-declared components should no longer carry feature business rules, session orchestration, or screen-specific state decisions. - -### 1.2 Features Become the Main Organizational Unit - -The codebase should stop being primarily organized by technical type such as `ui/screens`, `ui/viewmodel`, `data/repository`, and `network`. - -The main product capabilities should become first-class feature boundaries: - -- `auth` -- `conversation` -- `memoir` -- `payment` -- `profile` - -Each feature should own its own presentation, domain, data, and navigation concerns inside its boundary. - -### 1.3 State Ownership Becomes Explicit - -The project should use one consistent UI architecture model: - -- screen UI is driven from state -- state flows downward -- user actions flow upward -- screen-level state is owned by screen-level state holders -- transient UI element state stays local unless it must survive process recreation - -Global singletons should no longer be used as ad hoc UI-observable state stores. - -### 1.4 Data Layer Becomes a Real Boundary - -Repositories should become the only boundary that coordinates: - -- local persistence -- remote transport -- sync policy -- domain-facing data access - -Core product data should move toward a single clear source of truth per data set. For data that must support caching or offline continuity, local persistence should become the canonical read source. - -### 1.5 Domain Logic Is Used Selectively - -The project should not introduce a full domain layer everywhere. - -Domain use cases should be introduced only where business workflows are long-lived, stateful, or operationally complex, especially: - -- session recovery -- realtime conversation -- recording and segment upload flows -- payment order lifecycle -- authentication refresh and account state recovery - -Simple CRUD-like flows should remain repository-driven. - -### 1.6 Startup and Lifecycle Handling Become Intentional - -Startup responsibilities should be explicit and minimal. App-wide initialization should be separated from business state loading. Lifecycle-aware state collection, effect handling, and saved-state restoration should become standard conventions instead of screen-by-screen exceptions. - -### 1.7 Modularization Happens After Boundaries Stabilize - -The project should not begin with aggressive Gradle modularization. It should first stabilize package boundaries inside the current module, then split mature boundaries into Gradle modules with low coupling and high cohesion. - -### 1.8 Compose-First UI Policy Remains Intact - -The project should remain Compose-first. - -View Binding and Data Binding should be treated as compatibility tools, not as the primary UI architecture: - -- new feature screens should not be designed around XML binding -- future View-based interoperability should prefer View Binding -- Data Binding should only be used for retained XML-based surfaces if there is a concrete reason to keep them - -### 1.9 List and Data Scaling Is Deferred Until Foundations Are Stable - -Paging 3 should be introduced only after: - -- repository boundaries are stable -- list ownership is feature-scoped -- local and remote responsibilities are explicit - -It should be treated as a list-scaling layer, not as the starting point of the refactor. - ---- - -## 2. Current Structural Issues - -The current codebase already contains useful refactor direction, especially the emerging `ports` and `adapters` structure in conversation, memoir, payment, and profile. However, the project still has project-wide structural problems that prevent the architecture from being consistent. - -The main issues are: - -- `:app` is the only Gradle module, so all concerns are compiled and coupled together -- app-wide entry points still coordinate business state directly -- feature code is split across horizontal folders such as `ui/`, `data/`, `network/`, `payment/`, and `feature/` -- state is stored across ViewModels, local Compose state, singleton objects, and startup code without one shared ownership model -- session, settings, and configuration responsibilities are spread across `MainActivity`, `LifeEchoApp`, `TokenManager`, `AppSettings`, and `AppConfig` -- repositories are present, but local/remote/source-of-truth rules are not yet uniformly enforced -- tests are not yet a strong enough architecture guardrail for a project-wide refactor - -Representative current files and directories: - -- `app-android/app/src/main/java/com/huaga/life_echo/MainActivity.kt` -- `app-android/app/src/main/java/com/huaga/life_echo/app/LifeEchoApp.kt` -- `app-android/app/src/main/java/com/huaga/life_echo/app/AppContainer.kt` -- `app-android/app/src/main/java/com/huaga/life_echo/navigation/` -- `app-android/app/src/main/java/com/huaga/life_echo/ui/screens/` -- `app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/` -- `app-android/app/src/main/java/com/huaga/life_echo/data/` -- `app-android/app/src/main/java/com/huaga/life_echo/network/` -- `app-android/app/src/main/java/com/huaga/life_echo/payment/` -- `app-android/app/src/main/java/com/huaga/life_echo/feature/` - ---- - -## 3. Target Structure - -This plan uses a two-stage target structure: - -- first stabilize feature boundaries inside the existing `:app` module -- then split stable boundaries into Gradle modules - -### 3.1 Intermediate Target Structure Inside the Current `:app` Module - -```text -app-android/app/src/main/java/com/huaga/life_echo/ - app/ - entry/ - navigation/ - startup/ - di/ - core/ - common/ - model/ - ui/ - datastore/ - database/ - network/ - testing/ - feature/ - auth/ - presentation/ - domain/ - data/ - navigation/ - conversation/ - presentation/ - domain/ - data/ - navigation/ - memoir/ - presentation/ - domain/ - data/ - navigation/ - payment/ - presentation/ - domain/ - data/ - navigation/ - profile/ - presentation/ - domain/ - data/ - navigation/ -``` - -### 3.2 Final Target Gradle Module Structure - -```text -app-android/ - app/ - core/ - common/ - model/ - ui/ - network/ - database/ - datastore/ - testing/ - feature/ - auth/ - conversation/ - memoir/ - payment/ - profile/ -``` - -### 3.3 File and Directory Migration Intent - -The following high-level migrations define the desired end state: - -- `MainActivity.kt` -> `app/entry/` -- `LifeEchoApp.kt`, `AppContainer.kt` -> `app/startup/` and `app/di/` -- `navigation/` -> `app/navigation/` and feature-scoped `navigation/` -- `ui/screens/` -> `feature/*/presentation/` -- `ui/viewmodel/` -> `feature/*/presentation/` -- `data/repository/` -> `feature/*/data/` or `core/*` depending on scope -- `data/database/` -> `core/database/` -- `data/preferences/` and settings/session storage -> `core/datastore/` -- `data/auth/` -> `core/datastore/` or `feature/auth/data/` depending on responsibility -- `network/` -> `core/network/` -- `payment/` -> `feature/payment/` -- shared reusable Compose UI -> `core/ui/` -- generic helpers in `utils/` -> redistributed into `core/common/`, `core/ui/`, or feature-local packages - ---- - -## 4. Refactor Steps - -The steps below are intentionally abstract. Each step defines the work boundary, the affected files or directories, the target structure after the step, and the architectural outcome that should be achieved before moving on. - -### Step 1: Establish the Project-Wide Architecture Contract - -**Objective** - -Create one explicit architecture baseline for `app-android` so that subsequent refactors are judged against a stable target instead of local conventions. - -**Primary Current Files and Directories** - -- `app-android/settings.gradle.kts` -- `app-android/app/build.gradle.kts` -- `app-android/app/src/main/AndroidManifest.xml` -- `app-android/app/src/main/java/com/huaga/life_echo/` -- `docs/plans/` - -**Target Structural Result** - -- one documented architecture contract in `docs/plans/` -- one agreed package naming policy for `app/`, `core/`, and `feature/` -- one agreed migration rule for what stays in `:app` for now and what becomes module-ready later - -**Architecture Guidance** - -- define feature boundaries before module boundaries -- define layer responsibilities before moving files -- stop adding new files into horizontal buckets such as `ui/screens` and `ui/viewmodel` -- treat this step as the governance layer for all following steps - -### Step 2: Thin the App Shell and Entry Points - -**Objective** - -Reduce `Application`, `Activity`, and app-level navigation to shell responsibilities only. - -**Primary Current Files and Directories** - -- `app-android/app/src/main/java/com/huaga/life_echo/MainActivity.kt` -- `app-android/app/src/main/java/com/huaga/life_echo/app/LifeEchoApp.kt` -- `app-android/app/src/main/java/com/huaga/life_echo/app/AppContainer.kt` -- `app-android/app/src/main/java/com/huaga/life_echo/navigation/` -- `app-android/app/src/main/java/com/huaga/life_echo/config/AppConfig.kt` -- `app-android/app/src/main/AndroidManifest.xml` -- `app-android/app/src/main/java/com/huaga/life_echo/wxapi/` - -**Target Structural Result** - -```text -app/ - entry/ - navigation/ - startup/ - di/ -``` - -**Architecture Guidance** - -- `MainActivity` becomes a host for root Compose content and system UI coordination only -- `LifeEchoApp` becomes startup assembly, not business bootstrap -- app navigation owns route composition, not feature decision logic -- manifest-declared components are treated as system entry points with clear ownership -- App Startup may be used only for truly app-wide initialization dependencies that need explicit ordering or library-style setup - -### Step 3: Normalize Shared Core Capabilities - -**Objective** - -Convert the current cross-cutting infrastructure into explicit `core` capabilities so features depend on stable shared boundaries rather than miscellaneous utility folders. - -**Primary Current Files and Directories** - -- `app-android/app/src/main/java/com/huaga/life_echo/network/` -- `app-android/app/src/main/java/com/huaga/life_echo/network/runtime/` -- `app-android/app/src/main/java/com/huaga/life_echo/data/database/` -- `app-android/app/src/main/java/com/huaga/life_echo/data/preferences/` -- `app-android/app/src/main/java/com/huaga/life_echo/data/auth/` -- `app-android/app/src/main/java/com/huaga/life_echo/ui/theme/` -- `app-android/app/src/main/java/com/huaga/life_echo/ui/components/common/` -- `app-android/app/src/main/java/com/huaga/life_echo/utils/` - -**Target Structural Result** - -```text -core/ - common/ - model/ - ui/ - datastore/ - database/ - network/ - testing/ -``` - -**Architecture Guidance** - -- `core/network` owns transport primitives, adapters, runtime providers, and network policy -- `core/database` owns Room entities, DAOs, and database assembly -- `core/datastore` owns preference-like and session-like persisted app data -- `core/ui` owns reusable design-system-level Compose primitives, not feature-specific widgets -- `utils/` should not continue to grow as a miscellaneous bucket - -### Step 4: Reorganize Features In Place Before Splitting Modules - -**Objective** - -Move product behavior out of horizontal technical folders into coherent feature boundaries while staying inside the current `:app` module. - -**Primary Current Files and Directories** - -- `app-android/app/src/main/java/com/huaga/life_echo/ui/screens/` -- `app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/` -- `app-android/app/src/main/java/com/huaga/life_echo/feature/` -- `app-android/app/src/main/java/com/huaga/life_echo/payment/` -- `app-android/app/src/main/java/com/huaga/life_echo/data/repository/` - -**Target Structural Result** - -```text -feature/ - auth/ - presentation/ - domain/ - data/ - navigation/ - conversation/ - presentation/ - domain/ - data/ - navigation/ - memoir/ - presentation/ - domain/ - data/ - navigation/ - payment/ - presentation/ - domain/ - data/ - navigation/ - profile/ - presentation/ - domain/ - data/ - navigation/ -``` - -**Architecture Guidance** - -- each feature owns its screen contracts, screen state holders, feature repositories, and feature navigation mapping -- feature-local reusable UI stays inside the feature until it proves to be truly cross-feature -- cross-feature reuse should be promoted to `core`, not copied into multiple features -- the existing `ports/adapters` direction should be preserved and completed, not replaced - -### Step 5: Standardize UI State, Events, and Saved State Rules - -**Objective** - -Replace the current mixed state model with one consistent policy for screen state, local UI state, saved state, and navigation handling. - -**Primary Current Files and Directories** - -- `app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/` -- `app-android/app/src/main/java/com/huaga/life_echo/ui/screens/` -- `app-android/app/src/main/java/com/huaga/life_echo/navigation/` -- `app-android/app/src/main/java/com/huaga/life_echo/data/auth/TokenManager.kt` -- `app-android/app/src/main/java/com/huaga/life_echo/ui/settings/AppSettings.kt` - -**Target Structural Result** - -- feature presentation packages expose explicit screen state contracts -- screen-level state ownership is moved out of app-wide singleton objects -- route and navigation decisions are driven by UI state and route state, not ad hoc callbacks from global singletons - -**Architecture Guidance** - -- screen state belongs to screen-level state holders -- short-lived UI element state stays local to Compose unless survival across recreation is required -- `SavedStateHandle` is used for screen-restorable business state when needed -- `rememberSaveable` is used only for UI-local restorable state -- navigation is handled as a UI concern, not as a one-off event bus from ViewModel to UI -- `collectAsStateWithLifecycle` becomes the standard collection pattern for screen state -- `LiveData` should not become the primary state model in this refactor - -### Step 6: Rebuild Session, Settings, and App Configuration Boundaries - -**Objective** - -Separate app configuration, user session state, and user preferences into explicit persisted boundaries instead of process-wide singleton state surfaces. - -**Primary Current Files and Directories** - -- `app-android/app/src/main/java/com/huaga/life_echo/config/AppConfig.kt` -- `app-android/app/src/main/java/com/huaga/life_echo/data/auth/TokenManager.kt` -- `app-android/app/src/main/java/com/huaga/life_echo/data/preferences/TokenPreferences.kt` -- `app-android/app/src/main/java/com/huaga/life_echo/ui/settings/AppSettings.kt` - -**Target Structural Result** - -```text -core/ - datastore/ - session/ - settings/ -app/ - startup/ - di/ -``` - -**Architecture Guidance** - -- persistent preferences move behind DataStore-backed boundaries -- session state becomes a data concern, not a UI-observable global object -- environment and server configuration become startup/configuration concerns, not feature-level global reads -- one ownership model should exist for dark mode, typography preferences, session restoration, and app startup session checks - -### Step 7: Make the Data Layer Uniform Across Features - -**Objective** - -Move all features toward the same repository and data-source model so data access stops varying by feature. - -**Primary Current Files and Directories** - -- `app-android/app/src/main/java/com/huaga/life_echo/data/repository/` -- `app-android/app/src/main/java/com/huaga/life_echo/data/database/` -- `app-android/app/src/main/java/com/huaga/life_echo/network/models/` -- `app-android/app/src/main/java/com/huaga/life_echo/feature/*/ports/` -- `app-android/app/src/main/java/com/huaga/life_echo/feature/*/adapters/` - -**Target Structural Result** - -```text -feature/*/ - data/ - local/ - remote/ - repository/ - mapper/ -``` - -**Architecture Guidance** - -- features read from repositories, not directly from transport adapters -- local and remote responsibilities are separated -- mapping responsibility is explicit instead of leaking DTOs through presentation -- source-of-truth rules are documented per data set -- offline-first behavior is applied where it adds product value and operational safety - -### Step 8: Introduce Domain Workflows Only Where Complexity Demands It - -**Objective** - -Extract only the business workflows that are too complex to live directly in screen state holders or repositories. - -**Primary Current Files and Directories** - -- `app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModel.kt` -- `app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/AuthViewModel.kt` -- `app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/PaymentViewModel.kt` -- `app-android/app/src/main/java/com/huaga/life_echo/feature/voice/` -- `app-android/app/src/main/java/com/huaga/life_echo/network/WebSocketClient.kt` - -**Target Structural Result** - -```text -feature/*/ - domain/ - usecase/ - coordinator/ - policy/ -``` - -**Architecture Guidance** - -- use cases should be introduced only for multi-step workflows -- orchestration logic should move out of oversized ViewModels -- realtime coordination, recording coordination, payment lifecycle handling, and session recovery are the first candidates -- avoid creating a ceremonial domain layer for simple screens - -### Step 9: Stabilize View Interop Policy - -**Objective** - -Make the project’s View Binding and Data Binding policy explicit so future Android UI work does not reintroduce architectural ambiguity. - -**Primary Current Files and Directories** - -- `app-android/app/build.gradle.kts` -- `app-android/app/src/main/res/` -- any future View-based integration surfaces - -**Target Structural Result** - -- Compose remains the default presentation technology -- XML-based views, if required, are isolated and clearly owned - -**Architecture Guidance** - -- use View Binding for retained or third-party-interoperability View surfaces -- do not introduce Data Binding as a broad architectural pattern for new screens -- if Data Binding is ever retained for a legacy surface, keep it isolated to that surface and do not spread it into feature-wide state architecture - -### Step 10: Split Stable Boundaries into Gradle Modules - -**Objective** - -Only after package boundaries are stable, move mature `core` and `feature` areas into Gradle modules. - -**Primary Current Files and Directories** - -- `app-android/settings.gradle.kts` -- `app-android/build.gradle.kts` -- `app-android/app/build.gradle.kts` -- all stabilized `app/`, `core/`, and `feature/` directories - -**Target Structural Result** - -```text -:app -:core:common -:core:model -:core:ui -:core:network -:core:database -:core:datastore -:core:testing -:feature:auth -:feature:conversation -:feature:memoir -:feature:payment -:feature:profile -``` - -**Architecture Guidance** - -- split modules only after ownership is already clear in packages -- start with `core` modules that have the least feature churn -- move feature modules after their presentation/data/domain contracts are stable -- keep dependency direction one-way: `app -> feature -> core` - -### Step 11: Add List-Scale Infrastructure and Paging Where Needed - -**Objective** - -Introduce Paging 3 only after feature repositories and local/remote ownership are stable. - -**Primary Current Files and Directories** - -- `app-android/app/src/main/java/com/huaga/life_echo/ui/screens/ConversationListScreen.kt` -- `app-android/app/src/main/java/com/huaga/life_echo/ui/screens/MyMemoirScreen.kt` -- `app-android/app/src/main/java/com/huaga/life_echo/ui/screens/MyOrdersScreen.kt` -- corresponding future feature repositories and local data sources - -**Target Structural Result** - -- list-heavy features gain an explicit paging layer inside feature data and presentation - -**Architecture Guidance** - -- use Paging only where data volume or refresh cost justifies it -- where local storage is the primary read surface, adopt the network-plus-database paging model -- load state, transform, migration, and test strategy should be planned together rather than introduced piecemeal - -### Step 12: Finish with Architecture Cleanup and Convergence - -**Objective** - -Remove transitional structures and ensure the project no longer mixes old and new architecture styles. - -**Primary Current Files and Directories** - -- `app-android/app/src/main/java/com/huaga/life_echo/ui/` -- `app-android/app/src/main/java/com/huaga/life_echo/data/` -- `app-android/app/src/main/java/com/huaga/life_echo/network/` -- `app-android/app/src/main/java/com/huaga/life_echo/utils/` -- any transitional compatibility wrappers created during the migration - -**Target Structural Result** - -- obsolete horizontal folders are removed or reduced to clearly scoped core packages -- feature ownership is visible from the top-level directory structure -- app-wide conventions are uniform - -**Architecture Guidance** - -- do not leave the codebase half horizontal and half feature-based -- remove transitional adapters once all callers move to the new boundaries -- update architecture documentation whenever the actual package or module graph changes - ---- - -## 5. Step Ordering and Dependency Logic - -The steps above should be executed in this order: - -1. establish architecture contract -2. thin the app shell -3. normalize shared core capabilities -4. reorganize features in place -5. standardize state and saved-state rules -6. rebuild session and settings boundaries -7. make repositories uniform -8. extract complex domain workflows -9. stabilize View interop policy -10. split stable packages into modules -11. add paging where justified -12. remove transitional structure - -This sequence matters because the project should first define ownership, then stabilize boundaries, then modularize, and only then add scaling infrastructure. - ---- - -## 6. Explicit Non-Goals - -The following are not goals of this plan: - -- a full rewrite of the Android client -- a blanket migration to Data Binding -- immediate introduction of Hilt or another DI framework as the primary refactor goal -- immediate Paging adoption across all lists -- preserving the current horizontal package structure -- treating every feature as if it needs a full domain layer - ---- - -## 7. Expected End State - -When this plan is complete, the Android project should have: - -- a thin app shell -- feature-oriented ownership -- explicit state-holder rules -- repository-centered data boundaries -- Compose-first presentation -- DataStore-backed app preferences and session boundaries -- selective domain orchestration for complex workflows -- stable `core` and `feature` packages ready for modularization -- Gradle modules aligned with product boundaries instead of implementation detail folders - -At that point, future Android work should default to adding behavior inside existing feature boundaries rather than creating new top-level horizontal folders. diff --git a/docs/todo/various.md b/docs/todo/various.md deleted file mode 100644 index 1145695..0000000 --- a/docs/todo/various.md +++ /dev/null @@ -1,10 +0,0 @@ -.env 中硬编码了密钥 -.env 文件中直接写入了 TENCENT_COS_SECRET_ID 和 TENCENT_COS_SECRET_KEY: - -TENCENT_COS_SECRET_ID=AKIDa2ILCwUr56uVt31oU0JOHxPfGhvvkLiq -TENCENT_COS_SECRET_KEY=xiFbjlZ9XheS2NWYLvHRPAh2A5nGYcR2 -官方文档强调"建议使用子账号密钥,授权遵循最小权限指引"。请确认: - -这是否为子账号密钥(而非主账号密钥) -该密钥权限是否已限定为最小范围(只有 cos:PutObject 和 cos:GetObject 权限,仅限目标 bucket) -.env 是否已加入 .gitignore(防止密钥泄露到 git 仓库) \ No newline at end of file diff --git a/docs/v1_features_client.md b/docs/v1_features_client.md deleted file mode 100644 index 15e97c1..0000000 --- a/docs/v1_features_client.md +++ /dev/null @@ -1,394 +0,0 @@ -# 岁月时书 - 客户端需求文档 V1 - -> **产品定位**:一款帮助用户(尤其是中老年人)通过 AI 引导对话,将人生故事整理成回忆录的移动应用。 - ---- - -## 1. 产品概述 - -### 1.1 核心价值 - -- 让每个人都能轻松记录自己的人生故事 -- 通过 AI 引导式对话,降低写作门槛 -- 自动将对话内容整理成结构化的回忆录 -- 提供舒适的阅读和分享体验 - -### 1.2 目标用户 - -- 主要用户:中老年人群(50岁以上) -- 次要用户:想要记录家族故事的年轻人 -- 使用场景:日常空闲时间、与家人交流后、睡前回忆 - -### 1.3 设计原则 - -- 简洁易用:界面清晰,操作简单 -- 温暖亲切:配色柔和,文案亲切 -- 无压力记录:不强制用户持续输入 -- 类微信体验:借鉴熟悉的交互模式 - ---- - -## 2. 功能模块 - -### 2.1 应用架构 - -``` -├── 对话列表(首页 Tab) -│ └── 聊天详情页 -├── 回忆录(Tab) -│ ├── 目录页 -│ ├── 章节阅读页 -│ └── 全文阅读页 -└── 我的(Tab) - ├── 个人信息 - ├── 套餐与付费 - └── 帮助与设置 -``` - ---- - -## 3. 用户故事 - -### EPIC 1:对话列表模块 - -#### US-1.1 查看对话列表 - -**用户故事** -> 作为用户,我希望打开应用后能看到与AI助手的对话入口,让我快速开始聊天。 - -**功能要求** -- 应用启动默认进入对话列表页 -- 页面标题显示「岁月时书」 -- V1 版本仅显示一个默认对话:「回忆录助手」 - - 头像(📖 emoji) - - 对话标题:回忆录助手 - - 最近一条消息预览(单行省略) - - 时间戳(刚刚/分钟前/小时前/日期) -- 点击对话项进入聊天详情 -- 不支持用户创建新对话(后续版本支持多 Agent) - -**界面要素** -- 顶部紫色背景 Header -- 对话列表区域(奶油色背景) -- 底部 Tab 导航栏 - -#### US-1.2 查看使用提示 - -**用户故事** -> 作为新用户,我希望看到引导提示,让我知道如何使用这个应用。 - -**功能要求** -- 对话列表底部显示「小贴士」卡片 -- 内容说明应用使用方式 -- 卡片样式温和不突兀 - ---- - -### EPIC 2:聊天详情模块 - -#### US-2.1 进入沉浸式聊天 - -**用户故事** -> 作为用户,我希望进入一个专注的聊天界面,就像在微信里和朋友聊天一样。 - -**功能要求** -- 顶部导航栏: - - 返回按钮(圆角方形) - - 对话标题(居中) - - 在线状态提示 -- 隐藏底部 Tab 导航 -- 消息列表支持滚动 -- 新消息自动滚动到底部 - -**界面要素** -- 紫色 Header 区域 -- 奶油色消息区域 -- 时间分隔线(如"今天 14:30") - -#### US-2.2 发送文字消息 - -**用户故事** -> 作为用户,我希望能输入文字来讲述我的回忆,就像发微信一样简单。 - -**功能要求** -- 底部输入区域: - - 多行文本输入框 - - 输入框自动扩展高度 - - 发送按钮(有文字时激活) -- 用户消息显示为右侧气泡(紫色背景) -- 发送后立即显示(乐观更新) -- 支持键盘弹出时自动调整布局 - -#### US-2.3 接收 AI 回复 - -**用户故事** -> 作为用户,我希望 AI 能主动提问和追问,帮助我把记忆说得更完整。 - -**功能要求** -- AI 消息显示为左侧气泡(白色背景) -- 显示「正在输入…」动画(三个跳动的点) -- AI 回复风格: - - 温和亲切 - - 引导式提问 - - 适时鼓励和肯定 - - 帮助回忆细节 - -**AI 引导示例** -- "这个故事真让人感动!😊 能再详细说说当时的场景吗?" -- "听起来是一段珍贵的回忆呢!那个时候您多大年纪?" -- "太有画面感了!这件事对您的人生有什么特别的影响吗?" - -#### US-2.4 语音输入 - -**用户故事** -> 作为用户,我希望能用语音讲述故事,打字太累了。 - -**功能要求** -- 输入区域显示麦克风图标 -- 点击切换到语音输入模式: - - 按住说话,松开发送 - - 上滑取消发送 - - 显示录音时长和波形动画 -- 语音消息发送后: - - 显示语音气泡(带时长标识) - - 后端自动转写为文字 - - AI 基于转写文字进行回复 -- 支持点击语音气泡播放 - -**技术要点** -- 使用 Expo AV 进行录音 -- 音频格式:AAC / M4A -- 最长录音时长:60秒 -- 上传至 OSS 后发送消息 - -**已知限制(待补方案)** -- 若长语音采用“边录边分段上传”,用户点击“取消录音”时,只能停止后续录制与后续切片上传。 -- 对于已经上传到服务端的分段,当前系统没有“整段作废/整轮撤销”的服务端丢弃机制。 -- 因此,现阶段无法严格保证“取消录音 = 本次整段语音完全不进入 ASR/Agent”。 -- 后续若要支持该语义,需要服务端增加基于 `voice_session_id` 的撤销/丢弃能力,并确保已上传分段不会继续进入转写、聚合和 AI 回复链路。 - -#### US-2.5 图片发送(后续版本) - -**用户故事** -> 作为用户,我希望能发老照片来丰富我的故事。 - -**功能要求(后续版本)** -- 输入区域显示更多(+)图标 -- 点击提示「功能开发中」 -- V2 版本支持从相册选择或拍照 - ---- - -### EPIC 3:回忆录模块 - -#### US-3.1 查看回忆录目录 - -**用户故事** -> 作为用户,我希望看到我的对话被整理成一本有章节的书,让我有成就感。 - -**功能要求** -- 页面标题显示「回忆录」 -- 书籍信息卡片: - - 书名(如"这一生") - - 副标题(如"我的回忆录") - - 最后更新时间 -- 章节列表: - - 章节编号和标题 - - 章节状态指示: - - ✅ 完整(complete) - - 🔄 部分完成(partial) - - ⏳ 待开始(pending) - - 页数显示(可选) - -**章节示例** -1. 童年与家庭 ✅ -2. 上学的日子 🔄 -3. 工作与事业 ⏳ -4. 爱情与婚姻 ⏳ - -#### US-3.2 阅读单个章节 - -**用户故事** -> 作为用户,我希望能像读书一样阅读已整理好的章节内容。 - -**功能要求** -- 点击章节进入阅读模式 -- 页面切换动画(从右滑入) -- 顶部导航: - - 返回目录按钮 - - 更多操作按钮(...) -- 章节内容展示: - - 章节标题 - - 正文内容(舒适排版) - - 支持段落引用样式 -- 滚动阅读体验 - -#### US-3.3 阅读全文 - -**用户故事** -> 作为用户,我希望能一口气读完整本回忆录,而不是一章章点开。 - -**功能要求** -- 目录页显示「阅读全文」浮动按钮 -- 点击进入全文阅读模式 -- 按章节顺序展示所有内容 -- 章节之间有分隔 - -#### US-3.4 导出与分享(占位功能) - -**用户故事** -> 作为用户,我希望能把回忆录分享给家人或导出成 PDF 打印。 - -**功能要求(V1 占位)** -- 阅读页更多菜单: - - 导出 PDF 选项 - - 分享选项 -- 点击后提示「功能开发中」 - ---- - -### EPIC 4:个人中心模块 - -#### US-4.1 查看个人信息 - -**用户故事** -> 作为用户,我希望在一个地方看到我的账户信息,知道自己是谁。 - -**功能要求** -- 个人信息展示: - - 头像(图标占位) - - 昵称 - - 当前套餐标识(如"免费体验版") - -#### US-4.2 套餐与付费 - -**用户故事** -> 作为用户,我希望知道如何升级获得更多功能。 - -**功能要求** -- 套餐设置区块: - - 升级套餐入口 - - 我的订单入口 -- 点击后提示「功能开发中」 - -#### US-4.3 帮助与支持 - -**用户故事** -> 作为用户,我希望遇到问题时能找到帮助。 - -**功能要求** -- 帮助区块: - - 常见问题入口 - - 反馈与客服入口 - - 关于我们入口 -- 底部显示版本号 - ---- - -## 4. 界面设计规范 - -### 4.1 配色方案 - -| 颜色名称 | 色值 | 使用场景 | -|---------|------|---------| -| Deep Purple | #200028 | 深色文字、强调元素 | -| Medium Purple | #6B4C7C | 主题色、Header 背景、按钮 | -| Slate Purple | #6A6478 | 次要文字 | -| Lavender | #E8E0EC | 浅色背景、分隔线 | -| Cream | #FAF8F5 | 主要内容区背景 | -| White | #FFFFFF | 卡片背景、气泡背景 | - -### 4.2 字体规范 - -- 大标题:32px, 600 weight -- 页面标题:24px, 600 weight -- 正文:15-16px, 400 weight -- 辅助文字:12-13px, 400 weight - -### 4.3 间距规范 - -- 页面边距:20px -- 卡片内边距:16px -- 元素间距:8-12px -- 圆角:12-16px - -### 4.4 交互规范 - -- 点击反馈:activeOpacity 0.7-0.8 -- 页面切换:300ms 滑动动画 -- 加载状态:骨架屏或加载指示器 - ---- - -## 5. 技术要求 - -### 5.1 技术栈 - -- 框架:React Native + Expo -- 路由:Expo Router -- 动画:React Native Reanimated -- 图标:@expo/vector-icons (Ionicons) - -### 5.2 兼容性要求 - -- iOS:14.0+ -- Android:API Level 24+ (Android 7.0+) - -### 5.3 性能要求 - -- 首屏加载:< 2s -- 页面切换:< 300ms -- 消息发送响应:< 100ms(本地) - ---- - -## 6. V1 功能清单 - -### 核心功能 ✅ - -- [x] 对话列表展示 -- [x] 对话详情与消息交互 -- [x] 文字消息发送 -- [x] 语音消息发送(录音 + 转写) -- [x] AI 自动回复(真实 LLM) -- [x] 回忆录目录展示 -- [x] 章节内容阅读 -- [x] 全文阅读模式 -- [x] 个人中心基础信息 - -### 后续版本功能 ⏳ - -- [ ] 图片发送与 AI 理解 -- [ ] PDF 导出 -- [ ] 分享功能 -- [ ] 套餐升级与付费 -- [ ] 用户登录/注册 - ---- - -## 7. 后续迭代方向 - -### V1.1 增强功能 - -- 图片消息发送与 AI 理解 -- 回忆录编辑功能 -- 流式 AI 回复(打字机效果) - -### V1.2 多 Agent 支持 - -- 支持创建多个对话 -- 不同功能的 AI 助手(如:故事整理助手、照片回忆助手等) -- 对话管理(删除、归档) - -### V1.3 社交功能 - -- 分享到微信/朋友圈 -- 家庭成员协作 -- 回忆录评论 - -### V2.0 完整体验 - -- 用户登录注册 -- 付费套餐系统 -- PDF/实体书导出 -- 多语言支持 diff --git a/docs/v1_features_server.md b/docs/v1_features_server.md deleted file mode 100644 index 902fc35..0000000 --- a/docs/v1_features_server.md +++ /dev/null @@ -1,825 +0,0 @@ -# 岁月时书 - 服务端需求文档 V1 - -> **产品定位**:为「岁月时书」客户端提供 AI 对话、回忆录生成、用户管理等核心后端服务。 - ---- - -## 1. 系统概述 - -### 1.1 核心职责 - -- 管理用户对话与消息 -- 提供 AI 引导式对话能力 -- 自动整理对话生成回忆录 -- 用户账户与套餐管理 - -### 1.2 技术架构概览 - -``` -┌─────────────────────────────────────────────────────────┐ -│ 客户端 (App) │ -└─────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────┐ -│ API Gateway │ -│ (认证/限流/路由) │ -└─────────────────────────────────────────────────────────┘ - │ - ┌───────────────────┼───────────────────┐ - ▼ ▼ ▼ -┌───────────────┐ ┌───────────────┐ ┌───────────────┐ -│ 对话服务 │ │ 回忆录服务 │ │ 用户服务 │ -│ Chat Service │ │ Memoir Service│ │ User Service │ -└───────────────┘ └───────────────┘ └───────────────┘ - │ │ │ - ▼ ▼ ▼ -┌─────────────────────────────────────────────────────────┐ -│ 数据存储层 │ -│ (PostgreSQL / Redis / OSS) │ -└─────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────┐ -│ AI 服务层 │ -│ (LLM API / ASR 语音转写) │ -└─────────────────────────────────────────────────────────┘ -``` - ---- - -## 2. API 设计 - -### 2.1 基础约定 - -- **协议**:HTTPS -- **格式**:JSON -- **认证**:Bearer Token (JWT) -- **版本**:`/api/v1/` - -### 2.2 响应格式 - -**成功响应** -```json -{ - "code": 0, - "message": "success", - "data": { ... } -} -``` - -**错误响应** -```json -{ - "code": 10001, - "message": "错误描述", - "data": null -} -``` - -**错误码规范** -| 错误码范围 | 类型 | -|-----------|------| -| 10000-19999 | 通用错误 | -| 20000-29999 | 用户模块错误 | -| 30000-39999 | 对话模块错误 | -| 40000-49999 | 回忆录模块错误 | - ---- - -## 3. 功能模块 API - -### 3.1 用户模块 - -#### 3.1.1 用户注册(V1 占位) - -``` -POST /api/v1/auth/register -``` - -**请求参数** -```json -{ - "phone": "13800138000", - "code": "123456", - "nickname": "李明华" -} -``` - -**响应数据** -```json -{ - "user_id": "u_xxxxx", - "token": "jwt_token_xxxxx", - "expires_at": "2024-12-31T23:59:59Z" -} -``` - -#### 3.1.2 用户登录(V1 占位) - -``` -POST /api/v1/auth/login -``` - -**请求参数** -```json -{ - "phone": "13800138000", - "code": "123456" -} -``` - -#### 3.1.3 获取用户信息 - -``` -GET /api/v1/user/profile -``` - -**响应数据** -```json -{ - "user_id": "u_xxxxx", - "nickname": "李明华", - "avatar_url": null, - "plan_type": "free", - "plan_label": "免费体验版", - "created_at": "2024-01-01T00:00:00Z" -} -``` - -#### 3.1.4 更新用户信息 - -``` -PUT /api/v1/user/profile -``` - -**请求参数** -```json -{ - "nickname": "新昵称", - "avatar_url": "https://..." -} -``` - ---- - -### 3.2 对话模块 - -#### 3.2.1 获取对话列表 - -``` -GET /api/v1/conversations -``` - -**说明** -- V1 版本每个用户仅有一个默认对话「回忆录助手」 -- 用户首次访问时自动创建 -- 不支持用户手动创建新对话(后续版本支持多 Agent) - -**响应数据** -```json -{ - "conversations": [ - { - "id": "conv_xxxxx", - "title": "回忆录助手", - "avatar_emoji": "📖", - "last_message": "您想从哪里开始呢?可以聊聊童年...", - "last_message_at": "2024-01-15T14:30:00Z", - "unread_count": 0, - "type": "system" - } - ], - "total": 1, - "has_more": false -} -``` - -#### 3.2.2 创建新对话(后续版本) - -``` -POST /api/v1/conversations -``` - -**说明**:V1 暂不开放,后续版本支持多 Agent 时启用 - -**请求参数** -```json -{ - "type": "custom", - "title": "关于童年的回忆", - "agent_type": "memoir_assistant" -} -``` - -**响应数据** -```json -{ - "id": "conv_xxxxx", - "title": "关于童年的回忆", - "created_at": "2024-01-15T15:00:00Z" -} -``` - -#### 3.2.3 获取对话消息列表 - -``` -GET /api/v1/conversations/:id/messages -``` - -**查询参数** -| 参数 | 类型 | 说明 | -|-----|------|------| -| before | string | 游标,获取此消息之前的内容 | -| limit | int | 每页数量,默认 50 | - -**响应数据** -```json -{ - "messages": [ - { - "id": "msg_xxxxx", - "sender_type": "ai", - "content_type": "text", - "content": "您好!我是您的回忆录助手...", - "created_at": "2024-01-15T14:30:00Z" - }, - { - "id": "msg_yyyyy", - "sender_type": "user", - "content_type": "text", - "content": "我想聊聊我的童年", - "created_at": "2024-01-15T14:31:00Z" - } - ], - "has_more": false -} -``` - -#### 3.2.4 发送消息 - -``` -POST /api/v1/conversations/:id/messages -``` - -**请求参数(文字消息)** -```json -{ - "content_type": "text", - "content": "我出生在一个小村庄..." -} -``` - -**请求参数(语音消息)** -```json -{ - "content_type": "audio", - "content": "https://oss.example.com/audio/xxxxx.m4a", - "metadata": { - "duration": 15 - } -} -``` - -**请求参数(图片消息,后续版本)** -```json -{ - "content_type": "image", - "content": "https://oss.example.com/images/xxxxx.jpg", - "metadata": { - "width": 1080, - "height": 720 - } -} -``` - -**响应数据** -```json -{ - "user_message": { - "id": "msg_xxxxx", - "sender_type": "user", - "content_type": "text", - "content": "我出生在一个小村庄...", - "created_at": "2024-01-15T14:35:00Z" - }, - "ai_response": { - "id": "msg_yyyyy", - "sender_type": "ai", - "content_type": "text", - "content": "这个开头真让人期待!能再详细说说那个村庄是什么样的吗?", - "created_at": "2024-01-15T14:35:02Z" - } -} -``` - -#### 3.2.5 上传音频文件 - -``` -POST /api/v1/upload/audio -Content-Type: multipart/form-data -``` - -**请求参数** -| 参数 | 类型 | 说明 | -|-----|------|------| -| file | File | 音频文件(M4A/AAC)| -| duration | int | 音频时长(秒)| - -**响应数据** -```json -{ - "url": "https://oss.example.com/audio/xxxxx.m4a", - "duration": 15, - "transcription": "我出生在一个小村庄,那时候家里很穷..." -} -``` - -**处理流程** -1. 接收音频文件 -2. 上传至 OSS 存储 -3. 调用语音识别服务(ASR)转写 -4. 返回音频 URL 和转写文本 - -#### 3.2.6 流式消息接口(可选增强) - -``` -POST /api/v1/conversations/:id/messages/stream -``` - -**说明**:使用 Server-Sent Events (SSE) 实现 AI 回复的流式输出 - -**事件类型** -``` -event: message_start -data: {"message_id": "msg_xxxxx"} - -event: content_delta -data: {"delta": "这个"} - -event: content_delta -data: {"delta": "开头"} - -event: message_end -data: {"message_id": "msg_xxxxx"} -``` - ---- - -### 3.3 回忆录模块 - -#### 3.3.1 获取用户回忆录 - -``` -GET /api/v1/memoir -``` - -**响应数据** -```json -{ - "id": "memoir_xxxxx", - "title": "这一生", - "subtitle": "我的回忆录", - "updated_at": "2024-01-15T14:35:00Z", - "word_count": 3500, - "chapter_count": 4 -} -``` - -#### 3.3.2 获取章节列表 - -``` -GET /api/v1/memoir/chapters -``` - -**响应数据** -```json -{ - "chapters": [ - { - "id": "chapter_001", - "number": 1, - "title": "童年与家庭", - "status": "complete", - "page_count": 3, - "word_count": 1200, - "updated_at": "2024-01-15T14:00:00Z" - }, - { - "id": "chapter_002", - "number": 2, - "title": "上学的日子", - "status": "partial", - "page_count": 2, - "word_count": 600, - "updated_at": "2024-01-15T14:30:00Z" - }, - { - "id": "chapter_003", - "number": 3, - "title": "工作与事业", - "status": "pending", - "page_count": 0, - "word_count": 0, - "updated_at": null - } - ] -} -``` - -#### 3.3.3 获取章节内容 - -``` -GET /api/v1/memoir/chapters/:id -``` - -**响应数据** -```json -{ - "id": "chapter_001", - "number": 1, - "title": "童年与家庭", - "status": "complete", - "content": "我出生在一个普通的农村家庭...\n\n\"日子虽然清苦,但那时候的快乐是最纯粹的。\"\n\n父亲是村里的木匠...", - "images": [ - { - "id": "img_001", - "url": "https://...", - "caption": "老家的院子", - "position": 3 - } - ], - "updated_at": "2024-01-15T14:00:00Z" -} -``` - -#### 3.3.4 获取完整回忆录 - -``` -GET /api/v1/memoir/full -``` - -**响应数据** -```json -{ - "id": "memoir_xxxxx", - "title": "这一生", - "subtitle": "我的回忆录", - "chapters": [ - { - "id": "chapter_001", - "number": 1, - "title": "童年与家庭", - "content": "...", - "images": [] - }, - { - "id": "chapter_002", - "number": 2, - "title": "上学的日子", - "content": "...", - "images": [] - } - ], - "updated_at": "2024-01-15T14:35:00Z" -} -``` - -#### 3.3.5 导出回忆录(V1 占位) - -``` -POST /api/v1/memoir/export -``` - -**请求参数** -```json -{ - "format": "pdf", - "chapters": ["chapter_001", "chapter_002"] -} -``` - -**响应数据** -```json -{ - "task_id": "export_xxxxx", - "status": "processing", - "estimated_time": 30 -} -``` - -``` -GET /api/v1/memoir/export/:task_id -``` - -**响应数据** -```json -{ - "task_id": "export_xxxxx", - "status": "completed", - "download_url": "https://...", - "expires_at": "2024-01-16T14:35:00Z" -} -``` - ---- - -## 4. 数据模型 - -### 4.1 数据库 Schema - -#### 用户表 (users) - -| 字段 | 类型 | 说明 | -|-----|------|------| -| id | VARCHAR(32) | 主键,如 u_xxxxx | -| phone | VARCHAR(20) | 手机号,唯一 | -| nickname | VARCHAR(50) | 昵称 | -| avatar_url | VARCHAR(500) | 头像 URL | -| plan_type | ENUM | 套餐类型:free/premium | -| created_at | TIMESTAMP | 创建时间 | -| updated_at | TIMESTAMP | 更新时间 | - -#### 对话表 (conversations) - -| 字段 | 类型 | 说明 | -|-----|------|------| -| id | VARCHAR(32) | 主键,如 conv_xxxxx | -| user_id | VARCHAR(32) | 用户 ID,外键 | -| title | VARCHAR(100) | 对话标题 | -| type | ENUM | 类型:system/custom | -| avatar_emoji | VARCHAR(10) | 头像 emoji | -| last_message | TEXT | 最后一条消息内容 | -| last_message_at | TIMESTAMP | 最后消息时间 | -| created_at | TIMESTAMP | 创建时间 | -| updated_at | TIMESTAMP | 更新时间 | - -#### 消息表 (messages) - -| 字段 | 类型 | 说明 | -|-----|------|------| -| id | VARCHAR(32) | 主键,如 msg_xxxxx | -| conversation_id | VARCHAR(32) | 对话 ID,外键 | -| sender_type | ENUM | 发送者:user/ai | -| content_type | ENUM | 内容类型:text/image/audio | -| content | TEXT | 消息内容或资源 URL | -| metadata | JSON | 扩展数据(图片尺寸、音频时长等)| -| created_at | TIMESTAMP | 创建时间 | - -#### 回忆录表 (memoirs) - -| 字段 | 类型 | 说明 | -|-----|------|------| -| id | VARCHAR(32) | 主键,如 memoir_xxxxx | -| user_id | VARCHAR(32) | 用户 ID,外键,唯一 | -| title | VARCHAR(100) | 书名 | -| subtitle | VARCHAR(200) | 副标题 | -| word_count | INT | 总字数 | -| created_at | TIMESTAMP | 创建时间 | -| updated_at | TIMESTAMP | 更新时间 | - -#### 章节表 (memoir_chapters) - -| 字段 | 类型 | 说明 | -|-----|------|------| -| id | VARCHAR(32) | 主键,如 chapter_xxxxx | -| memoir_id | VARCHAR(32) | 回忆录 ID,外键 | -| number | INT | 章节序号 | -| title | VARCHAR(100) | 章节标题 | -| content | TEXT | 章节内容(Markdown) | -| status | ENUM | 状态:pending/partial/complete | -| word_count | INT | 字数 | -| created_at | TIMESTAMP | 创建时间 | -| updated_at | TIMESTAMP | 更新时间 | - -#### 章节资源表 (chapter_assets) - -| 字段 | 类型 | 说明 | -|-----|------|------| -| id | VARCHAR(32) | 主键 | -| chapter_id | VARCHAR(32) | 章节 ID,外键 | -| type | ENUM | 类型:image/audio | -| url | VARCHAR(500) | 资源 URL | -| caption | VARCHAR(200) | 说明文字 | -| position | INT | 在内容中的位置 | -| created_at | TIMESTAMP | 创建时间 | - ---- - -## 5. AI 服务设计 - -### 5.1 语音识别服务(ASR) - -#### 5.1.1 功能说明 - -将用户上传的语音消息转写为文字,供 AI 对话使用。 - -#### 5.1.2 技术选型 - -| 服务商 | 优势 | 说明 | -|-------|------|------| -| 阿里云 ASR | 中文识别准确率高 | 推荐 | -| 腾讯云 ASR | 价格较低 | 备选 | -| Whisper API | 多语言支持好 | 可选 | - -#### 5.1.3 处理流程 - -``` -1. 接收音频文件(M4A/AAC 格式) -2. 调用 ASR 服务进行转写 -3. 返回转写文本 -4. 文本用于 AI 对话上下文 -``` - -#### 5.1.4 性能要求 - -- 转写延迟:< 3秒(60秒内音频) -- 识别准确率:> 95%(普通话) -- 支持方言识别(可选增强) - ---- - -### 5.2 对话 AI - -#### 5.2.1 系统提示词设计 - -```markdown -你是一位温和、有耐心的回忆录采访助手。你的任务是通过对话帮助用户回忆和讲述他们的人生故事。 - -## 角色特点 -- 像一位老朋友一样亲切 -- 善于倾听和共情 -- 会适时提出引导性问题 -- 帮助用户回忆细节 - -## 对话原则 -1. 每次回复要简短(1-3句话) -2. 多用鼓励和肯定的语气 -3. 提问要具体,避免开放性太强 -4. 关注时间、地点、人物、感受 -5. 适当使用 emoji 增加亲切感 - -## 引导方向 -- 童年与家庭 -- 求学经历 -- 工作与事业 -- 爱情与婚姻 -- 重要人生节点 -- 难忘的经历 - -## 注意事项 -- 不要一次问太多问题 -- 如果用户情绪低落,给予安慰 -- 尊重用户的隐私边界 -- 语音消息转写可能有错别字,根据上下文理解用户意图 -``` - -#### 5.2.2 技术选型 - -| 服务商 | 模型 | 说明 | -|-------|------|------| -| OpenAI | GPT-4o / GPT-4o-mini | 效果好,成本适中 | -| Claude | Claude 3.5 Sonnet | 对话质量高 | -| 智谱 AI | GLM-4 | 中文优化,国内访问快 | -| 阿里云 | 通义千问 | 国内合规,价格低 | - -#### 5.2.3 上下文管理 - -- 保留最近 20 轮对话作为上下文 -- 维护用户画像摘要(年龄、家庭背景等) -- 追踪已覆盖的话题,避免重复 - -### 5.3 回忆录生成 AI - -#### 5.2.1 触发条件 - -- 用户发送消息后异步触发 -- 或定时任务(每日)检查更新 - -#### 5.2.2 处理流程 - -``` -1. 提取新对话内容 -2. 分析内容主题 -3. 匹配/创建对应章节 -4. 将对话内容转化为叙述文字 -5. 合并到章节内容中 -6. 更新章节状态 -``` - -#### 5.2.3 内容转化提示词 - -```markdown -将以下对话内容转化为回忆录章节的叙述性文字。 - -要求: -1. 使用第一人称 -2. 保持自然流畅的叙事风格 -3. 保留重要细节和情感 -4. 适当引用原话作为点缀 -5. 按时间或逻辑顺序组织内容 - -对话内容: -{对话记录} - -当前章节已有内容: -{已有内容} - -请输出应追加到该章节的新内容: -``` - ---- - -## 6. 服务端要求 - -### 6.1 性能要求 - -| 指标 | 要求 | -|-----|------| -| API 响应时间 (P95) | < 500ms | -| AI 回复时间 | < 3s(首字)| -| 并发用户数 | 1000 | -| 消息吞吐量 | 100 msg/s | - -### 6.2 可靠性要求 - -| 指标 | 要求 | -|-----|------| -| 服务可用性 | 99.9% | -| 数据持久性 | 99.999999% | -| 故障恢复时间 | < 5min | - -### 6.3 安全要求 - -- 传输加密:TLS 1.2+ -- 数据加密:敏感数据 AES-256 加密存储 -- 认证:JWT + 刷新令牌机制 -- 授权:RBAC 权限控制 -- 审计:关键操作日志记录 - -### 6.4 合规要求 - -- 用户数据隔离 -- 数据导出能力 -- 账号注销与数据删除 -- 隐私政策声明 - ---- - -## 7. V1 实现范围 - -### 核心功能 ✅ - -- [x] 用户基础信息 API -- [x] 对话列表与详情 API -- [x] 文字消息发送与 AI 回复 -- [x] 语音消息上传与转写(ASR) -- [x] 语音消息 AI 回复 -- [x] 回忆录章节 API -- [x] LLM 对话能力(真实 AI) - -### 简化实现(V1) - -- [ ] 用户认证:允许匿名访问,通过设备 ID 识别 -- [ ] 回忆录生成:使用定时任务而非实时生成 - -### 后续版本功能 - -- [ ] 手机号注册登录 -- [ ] 图片消息上传与 AI 理解 -- [ ] PDF 导出 -- [ ] 付费套餐 -- [ ] 流式 AI 回复 - ---- - -## 8. 后续迭代方向 - -### V1.1 功能增强 - -- 图片消息支持(上传 + AI 理解) -- 流式 AI 回复(SSE) -- 实时回忆录生成 - -### V1.2 多 Agent 支持 - -- 支持创建多个对话 -- 不同类型的 AI 助手: - - 回忆录助手(默认) - - 故事整理助手 - - 照片回忆助手 -- 对话管理 API(删除、归档) - -### V1.3 用户体系 - -- 手机号注册登录 -- 微信登录 -- 套餐与付费 - -### V2.0 完整功能 - -- PDF/打印导出 -- 家庭成员协作 -- 回忆录分享 -- 多语言支持 -- 方言语音识别 diff --git a/docs/实施总结.md b/docs/实施总结.md deleted file mode 100644 index 5c1e488..0000000 --- a/docs/实施总结.md +++ /dev/null @@ -1,238 +0,0 @@ -# 实施总结 - -## 已完成的工作 - -### 1. 项目基础架构 ✅ - -#### 后端(FastAPI) -- ✅ 项目依赖配置(requirements.txt) -- ✅ 数据库模型设计(SQLAlchemy) -- ✅ 数据库连接和初始化 -- ✅ FastAPI 应用入口配置 -- ✅ CORS 中间件配置 - -#### Android -- ✅ Gradle 依赖配置(Ktor、Room、Compose Navigation 等) -- ✅ 项目结构搭建 -- ✅ 应用配置管理(AppConfig) - -### 2. 数据库层 ✅ - -#### 后端数据库 -- ✅ User 模型 -- ✅ Conversation 模型(包含对话阶段字段) -- ✅ Segment 模型(对话段落) -- ✅ Chapter 模型(章节) -- ✅ Book 模型(回忆录) -- ✅ 异步数据库会话管理 - -#### Android 数据库(Room) -- ✅ User Entity -- ✅ Conversation Entity -- ✅ ConversationSegment Entity -- ✅ Chapter Entity -- ✅ Book Entity -- ✅ DAO 接口(ConversationDao、ConversationSegmentDao、ChapterDao) -- ✅ AppDatabase 配置 -- ✅ Repository 层(ConversationRepository、ChapterRepository) - -### 3. Agent 系统 ✅ - -#### 提示词模板 -- ✅ 访谈问题库(6个阶段,30+问题) -- ✅ 对话 Agent 系统提示词 -- ✅ 整理 Agent 系统提示词 -- ✅ 章节分类规则 -- ✅ 文本改写规则 - -#### Agent / 编排 -- ✅ `ChatOrchestrator` + Specialist Agents(对话引导) - - 对话阶段检测 - - 动态问题选择 - - 会话历史以 DB 为真源 -- ✅ `MemoirOrchestrator` + Specialist Agents(回忆录整理) - - 章节分类 - - 口语到书面语改写 - - 章节生成和合并 - -### 4. WebSocket 实时通信 ✅ - -#### 后端 -- ✅ WebSocket 端点实现 -- ✅ 连接管理器(ConnectionManager) -- ✅ 消息类型定义(7种消息类型) -- ✅ 音频流处理 -- ✅ 实时转文字集成 -- ✅ Agent 回应生成 -- ✅ TTS 音频生成 -- ✅ 对话结束处理 -- ✅ 章节整理触发 - -#### Android -- ✅ WebSocket 客户端(Ktor) -- ✅ 连接管理 -- ✅ 消息发送/接收 -- ✅ 自动重连机制(指数退避) -- ✅ 消息数据模型(WebSocketMessage) - -### 5. REST API ✅ - -- ✅ 对话管理接口(创建、查询、结束) -- ✅ 章节查询接口(列表、详情、重新整理) -- ✅ 回忆录接口(当前回忆录、PDF 导出) - -### 6. 服务层 ✅ - -- ✅ ASR 服务(集成 OpenAI Whisper API) -- ✅ TTS 服务(集成 OpenAI TTS API) -- ✅ PDF 生成服务(支持中文字体) - -### 7. Android UI ✅ - -#### 页面实现 -- ✅ 创建回忆录页面(CreateMemoryScreen) - - WebSocket 连接状态显示 - - 实时转文字显示 - - Agent 回应显示 - - 开始/结束对话按钮 -- ✅ 我的回忆录页面(MyMemoirScreen) - - 目录列表 - - 章节阅读 - - PDF 导出按钮 -- ✅ 我的页面(ProfileScreen) - - 账户信息 - - 套餐与付费入口 - - 数据与隐私 - - 设置选项 - - 帮助入口 - -#### ViewModel -- ✅ CreateMemoryViewModel(对话管理) -- ✅ MyMemoirViewModel(章节管理) - -#### 导航 -- ✅ AppNavigation(Compose Navigation) -- ✅ MainActivity 集成 - -### 8. 功能模块 ✅ - -- ✅ 语音录制模块(VoiceRecorder) -- ✅ 网络服务(ApiService) -- ✅ 配置管理(AppConfig) - -### 9. 文档 ✅ - -- ✅ 开发计划文档 -- ✅ 数据库设计文档 -- ✅ README.md -- ✅ .env.example - -## 待完善的功能 - -### 高优先级 - -1. **用户认证** - - [ ] 微信登录集成 - - [ ] JWT Token 管理 - - [ ] 用户会话管理 - -2. **音频处理优化** - - [ ] 音频分块上传优化 - - [ ] 音频格式转换 - - [ ] 音频压缩 - -3. **错误处理** - - [ ] 完善的错误提示 - - [ ] 网络错误重试机制 - - [ ] 异常日志记录 - -4. **数据同步** - - [ ] Android 本地数据与服务器同步 - - [ ] 离线数据缓存 - - [ ] 冲突解决策略 - -### 中优先级 - -5. **UI/UX 优化** - - [ ] 加载状态显示 - - [ ] 空状态处理 - - [ ] 动画效果 - - [ ] 主题配色(使用 color.png) - -6. **性能优化** - - [ ] 数据库查询优化 - - [ ] 图片加载优化 - - [ ] 内存管理 - -7. **测试** - - [ ] 单元测试 - - [ ] 集成测试 - - [ ] UI 测试 - -### 低优先级 - -8. **功能增强** - - [ ] 章节编辑功能 - - [ ] 图片上传和管理 - - [ ] 分享功能 - - [ ] 多语言支持 - -9. **监控和日志** - - [ ] 应用性能监控 - - [ ] 错误追踪 - - [ ] 用户行为分析 - -## 技术债务 - -1. **配置管理** - - API 地址硬编码,需要改为配置化 - - 环境变量管理需要完善 - -2. **安全性** - - API Key 管理需要更安全的方式 - - WebSocket 连接需要认证 - -3. **代码质量** - - 部分代码需要重构 - - 需要添加更多注释和文档 - -## 下一步行动 - -1. **立即行动** - - 配置 OpenAI API Key - - 测试 WebSocket 连接 - - 测试 Agent 功能 - -2. **短期(1-2周)** - - 实现用户认证 - - 完善错误处理 - - UI/UX 优化 - -3. **中期(1个月)** - - 数据同步功能 - - 性能优化 - - 测试覆盖 - -4. **长期(2-3个月)** - - 功能增强 - - 监控和日志 - - 生产环境部署准备 - -## 已知问题 - -1. WebSocket 连接中用户 ID 目前使用默认值,需要从认证获取 -2. PDF 中文字体可能需要额外配置 -3. Android 模拟器访问 localhost 需要使用 10.0.2.2 -4. 部分功能需要实际 API Key 才能测试 - -## 总结 - -项目核心功能已基本实现,包括: -- ✅ 实时 WebSocket 对话 -- ✅ Agent 引导和整理 -- ✅ 数据库存储 -- ✅ Android UI -- ✅ PDF 导出 - -项目已具备基本运行能力,可以进行功能测试和进一步开发。 - diff --git a/docs/开发计划.md b/docs/开发计划.md deleted file mode 100644 index 187aabd..0000000 --- a/docs/开发计划.md +++ /dev/null @@ -1,855 +0,0 @@ -# 岁月时书 MVP 开发计划 - -> 基于实时 WebSocket 长连接的语音对话回忆录生成系统 - -## 目录 - -1. [项目概述](#项目概述) -2. [技术架构](#技术架构) -3. [数据库设计](#数据库设计) -4. [Agent 工作流设计](#agent-工作流设计) -5. [WebSocket 实时通信](#websocket-实时通信) -6. [开发任务分解](#开发任务分解) -7. [开发阶段规划](#开发阶段规划) -8. [关键技术实现](#关键技术实现) -9. [文件清单](#文件清单) -10. [注意事项](#注意事项) - ---- - -## 项目概述 - -### 核心功能 - -- **实时语音对话**:通过 WebSocket 长连接实现实时双向语音交互 -- **智能访谈引导**:基于访谈问题清单,Agent 动态引导用户讲述人生故事 -- **自动整理成书**:基于传记结构,将口语对话自动整理为优雅的回忆录章节 -- **阅读与导出**:优雅的电子书阅读体验和 PDF 导出功能 - -### 技术选型 - -| 模块 | 技术栈 | -|------|--------| -| Android 端 | Kotlin + Jetpack Compose + Ktor WebSocket + Room | -| 后端 | FastAPI + WebSocket + LangChain + SQLite | -| 数据库 | SQLite(Android 本地 + 后端服务器) | -| AI 框架 | LangChain(对话 Agent + 整理 Agent) | - ---- - -## 技术架构 - -### 整体架构图 - -```mermaid -graph TB - subgraph Android["Android 应用"] - UI[UI 层
Compose] - WS_Client[WebSocket 客户端
Ktor] - Room[Room 数据库
SQLite] - Voice[语音模块
录制/播放] - end - - subgraph Backend["后端服务"] - WS_Server[WebSocket 端点
FastAPI] - Conv_Agent[对话 Agent
LangChain] - Mem_Agent[整理 Agent
LangChain] - DB[(SQLite
数据库)] - ASR[ASR 服务
语音转文字] - TTS[TTS 服务
文字转语音] - end - - UI --> WS_Client - WS_Client <-->|WebSocket| WS_Server - Voice --> WS_Client - WS_Client --> Room - - WS_Server --> ASR - ASR --> Conv_Agent - Conv_Agent --> TTS - Conv_Agent --> DB - WS_Server --> Mem_Agent - Mem_Agent --> DB - DB --> WS_Server -``` - -### 数据流图 - -```mermaid -sequenceDiagram - participant User as 用户 - participant Android as Android App - participant WS as WebSocket - participant ASR as ASR 服务 - participant Agent as 对话 Agent - participant TTS as TTS 服务 - participant DB as 数据库 - - User->>Android: 开始录音 - Android->>WS: 连接 WebSocket - WS->>DB: 创建对话会话 - - loop 实时对话循环 - User->>Android: 说话 - Android->>WS: 发送音频块 - WS->>ASR: 转文字 - ASR->>WS: 返回转写文本 - WS->>Android: 显示转写文本 - WS->>Agent: 分析并生成回应 - Agent->>WS: 返回 Agent 回应 - WS->>Android: 显示 Agent 回应 - WS->>TTS: 生成语音 - TTS->>WS: 返回音频 - WS->>Android: 播放音频 - WS->>DB: 保存对话段落 - end - - User->>Android: 结束对话 - Android->>WS: 发送结束消息 - WS->>DB: 更新对话状态 - WS->>Agent: 触发整理 Agent - Agent->>DB: 生成章节 - DB->>Android: 同步章节数据 -``` - ---- - -## 数据库设计 - -### Android 本地数据库(Room) - -#### User 表 -```kotlin -@Entity(tableName = "users") -data class User( - @PrimaryKey val id: String, - val nickname: String, - val avatarUrl: String?, - val subscriptionType: String, - val createdAt: Long -) -``` - -#### Conversation 表 -```kotlin -@Entity(tableName = "conversations") -data class Conversation( - @PrimaryKey val id: String, - val userId: String, - val startedAt: Long, - val endedAt: Long?, - val durationSeconds: Int, - val summary: String?, - val currentTopic: String?, // 当前话题 - val conversationStage: String? // 对话阶段 -) -``` - -#### ConversationSegment 表 -```kotlin -@Entity(tableName = "conversation_segments") -data class ConversationSegment( - @PrimaryKey val id: String, - val conversationId: String, - val audioPath: String?, - val transcriptText: String, - val createdAt: Long, - val processed: Boolean, - val topicCategory: String? // 话题分类 -) -``` - -#### Chapter 表 -```kotlin -@Entity(tableName = "chapters") -data class Chapter( - @PrimaryKey val id: String, - val title: String, - val content: String, - val orderIndex: Int, - val status: String, // draft, completed - val updatedAt: Long, - val category: String // 章节分类 -) -``` - -#### Book 表 -```kotlin -@Entity(tableName = "books") -data class Book( - @PrimaryKey val id: String, - val userId: String, - val title: String, - val totalPages: Int, - val totalWords: Int, - val updatedAt: Long -) -``` - -### 后端数据库(SQLAlchemy) - -表结构与 Android 端对应,字段命名采用 snake_case。 - ---- - -## Agent 工作流设计 - -### 对话 Agent(基于访谈问题清单) - -#### 对话阶段枚举 - -```python -class ConversationStage(str, Enum): - CHILDHOOD = "childhood" # 童年 - EDUCATION = "education" # 教育 - CAREER = "career" # 事业 - FAMILY = "family" # 家庭 - BELIEFS = "beliefs" # 信念 - SUMMARY = "summary" # 人生总结 -``` - -#### 访谈问题库 - -**文件:** `api/agents/prompts/conversation_prompts.py` - -##### 童年阶段问题 -- "你是在哪里长大的?小时候周围的环境是什么样的,有哪些让你印象深刻的童年记忆?" -- "童年时期的你是个怎样的孩子?有没有做过什么淘气或有趣的事情,现在想起来还会让你发笑?" -- "能聊聊你小时候的家庭吗?比如父母是怎样的人,他们对你的成长有什么影响吗?" -- "小时候你有过什么梦想?那时候你最想长大后做什么?" - -##### 教育阶段问题 -- "上学的时候你是个怎么样的学生?你喜欢学校生活吗?" -- "在求学过程中,有没有哪位老师或同学对你影响特别大?能说说他们的故事吗?" -- "学生时代你参加过什么课外活动或者比赛吗?有没有哪段经历让你特别难忘?" -- "那时候你对未来有什么打算吗?比如毕业后想从事什么职业,或者希望过怎样的生活?" - -##### 事业阶段问题 -- "第一次走出校园开始工作时,你还记得当时的情景吗?当时你的心情怎么样,有发生什么难忘的事吗?" -- "你当初是怎么选择进入现在这个行业的?其中有什么契机或故事吗?" -- "在工作中有没有遇到过特别大的挑战或低谷?当时你是怎么挺过来的?" -- "职业生涯中有没有哪个成就或时刻让你特别自豪?能跟我分享一下那个故事吗?" -- "在事业的发展过程中,有哪些重要的转折点?比如跳槽、升职或者创业,这些经历对你意味着什么?" -- "回顾这一路,有哪些人对你的事业帮助最大或者影响最深?有没有特别想感谢的贵人或伙伴?" - -##### 家庭阶段问题 -- "可以聊聊你和你伴侣的故事吗?你们是怎么认识的,又是什么让你决定与他/她携手一生?" -- "孩子在你的生活中意味着什么?做父母的过程中,有没有让你特别骄傲或者难忘的瞬间?" -- "在家庭生活中,有没有什么传统或者特别的习惯,让你感到温馨和快乐?" -- "平时你和家人喜欢一起做些什么?周末或假日你们通常会怎么度过?" -- "你觉得家庭在你的人生中扮演了一个怎样的角色?" -- "工作和家庭要怎么兼顾呢?你是如何平衡事业和家庭的?在两边兼顾的时候有没有遇到困难,后来又是怎么克服的?" - -##### 信念阶段问题 -- "你人生中有没有一些一直坚守的信念或者座右铭?这些信念给了你怎样的力量或者影响?" -- "对你来说,哪些价值观是最重要的?这些价值观是受到哪些人或经历的影响而形成的呢?" -- "当你遇到困难和低谷的时候,是什么支撑着你坚持下去?" -- "你如何看待"成功"和"幸福"?对你来说它们分别意味着什么?" - -##### 人生总结问题 -- "回顾你走过的路,你觉得这一生中最重要的经验或教训是什么?" -- "在你的生活中,你最感激的人和事有哪些?有没有特别觉得自己很幸运的地方?" -- "如果能对年轻时候的自己说几句话,你会想告诉他/她什么?" -- "展望未来,你还有什么愿望或目标吗?有没有一直想尝试但还没来得及做的事情?" -- "最后,你希望家人和后代记住你是一个怎样的人?" - -#### 对话 Agent 系统提示词 - -``` -你是一位专业的人生故事访谈助手,擅长通过轻松自然的对话引导用户讲述他们的人生故事。 - -你的职责: -1. 根据用户已讲述的内容,判断当前应处于哪个访谈阶段(童年/教育/事业/家庭/信念/人生总结) -2. 在用户停顿或完成一个话题后,自然地提出下一个相关问题 -3. 使用温暖、鼓励的语气,让用户感到被倾听和理解 -4. 当用户讲述不够详细时,通过追问引导深入(如:"能多说一些关于...的细节吗?") -5. 识别对话的自然结束时机,或引导进入下一个阶段 - -对话原则: -- 一次只问一个问题,不要连续提问 -- 问题要自然、口语化,避免生硬 -- 根据用户的回答灵活调整,不要机械地按顺序提问 -- 当用户偏离话题时,温和地引导回来 -- 在用户讲述精彩故事时,给予积极反馈(如:"这个故事真有意思!") - -当前对话阶段:{current_stage} -已聊话题:{covered_topics} -用户最新回答:{user_latest_response} -``` - -#### 对话 Agent 工作流程 - -```mermaid -flowchart TD - A[接收用户语音转文字] --> B[分析内容主题] - B --> C{判断对话阶段} - C -->|童年| D1[童年阶段问题库] - C -->|教育| D2[教育阶段问题库] - C -->|事业| D3[事业阶段问题库] - C -->|家庭| D4[家庭阶段问题库] - C -->|信念| D5[信念阶段问题库] - C -->|总结| D6[人生总结问题库] - - D1 --> E[更新对话状态] - D2 --> E - D3 --> E - D4 --> E - D5 --> E - D6 --> E - - E --> F{用户回答完整性} - F -->|完整| G[给予肯定,提出下一个问题] - F -->|简短| H[追问细节] - F -->|话题转换| I[自然过渡] - - G --> J[生成 Agent 回应] - H --> J - I --> J - - J --> K[通过 WebSocket 返回] -``` - -### 回忆录整理 Agent(基于传记结构) - -#### 章节结构 - -根据名人传记研究,回忆录包含以下 8 个章节: - -1. **童年与成长背景** - - 出生和家庭背景 - - 童年故事和性格塑造 - - 早期梦想 - -2. **教育经历与青年时期** - - 学校生活 - - 师友关系 - - 世界观形成 - -3. **崭露头角(早期事业)** - - 职业起点 - - 初期机遇和挑战 - - 早期奋斗经历 - -4. **主要成就与巅峰时刻** - - 重大成就 - - 里程碑事件 - - 成功背后的因素 - -5. **挫折、挑战与重大转折** - - 低谷和挫折 - - 如何面对逆境 - - 从失败中汲取教训 - -6. **家庭与情感** - - 伴侣关系 - - 亲子关系 - - 家庭传统和习惯 - -7. **信念与价值观** - - 人生信念 - - 核心价值观 - - 对成功和幸福的定义 - -8. **人生总结** - - 重要经验和教训 - - 感激的人和事 - - 对未来的展望 - - 希望被记住的样子 - -#### 整理 Agent 系统提示词 - -``` -你是一位专业的传记作家和文字编辑,擅长将口语化的对话内容整理成优雅的书面语回忆录章节。 - -你的任务: -1. 接收对话段落文本(口语化) -2. 识别内容主题,归类到对应章节(童年/教育/事业/家庭/信念/总结) -3. 将口语化表达改写为书面语,保持原意和情感 -4. 生成合适的章节标题和段落结构 -5. 提取关键信息,形成连贯的叙述 -6. 建议插图位置(在描述场景、人物、地点的地方) - -改写原则: -- 保持用户的真实声音和情感 -- 使用优雅但不失亲切的书面语 -- 适当添加过渡句,使段落连贯 -- 保留生动的细节和对话 -- 去除口语中的"嗯"、"那个"等填充词 -- 保持时间顺序和逻辑清晰 - -章节分类规则: -- 童年相关 → "童年与成长背景" -- 学校、老师、同学 → "教育经历与青年时期" -- 工作、职业、成就 → "主要成就与巅峰时刻" 或 "崭露头角" -- 困难、挫折 → "挫折、挑战与重大转折" -- 伴侣、孩子、家庭生活 → "家庭与情感" -- 价值观、信念、座右铭 → "信念与价值观" -- 总结、感悟、展望 → "人生总结" - -输入内容:{conversation_segments} -已有章节:{existing_chapters} -``` - -#### 整理 Agent 工作流程 - -```mermaid -flowchart TD - A[接收对话段落] --> B[内容分析] - B --> C[文本分类
识别主题] - C --> D{判断章节归属} - - D -->|童年| E1[童年与成长背景] - D -->|教育| E2[教育经历与青年时期] - D -->|事业| E3[主要成就/崭露头角] - D -->|挫折| E4[挫折与重大转折] - D -->|家庭| E5[家庭与情感] - D -->|信念| E6[信念与价值观] - D -->|总结| E7[人生总结] - - E1 --> F[文本改写] - E2 --> F - E3 --> F - E4 --> F - E5 --> F - E6 --> F - E7 --> F - - F --> G[口语 → 书面语] - G --> H{章节是否存在} - H -->|不存在| I[创建新章节] - H -->|存在| J[合并内容] - - I --> K[保存到数据库] - J --> K -``` - ---- - -## WebSocket 实时通信 - -### 消息类型定义 - -```python -class MessageType(str, Enum): - CONNECT = "connect" # 建立连接 - AUDIO_CHUNK = "audio_chunk" # 音频数据块 - TRANSCRIPT = "transcript" # 转写文本 - AGENT_RESPONSE = "agent_response" # Agent 回应 - TTS_AUDIO = "tts_audio" # TTS 音频 - END_CONVERSATION = "end_conversation" # 结束对话 - ERROR = "error" # 错误消息 -``` - -### 消息格式 - -```json -{ - "type": "audio_chunk", - "conversation_id": "conv_123", - "data": { - "audio_base64": "...", - "chunk_index": 1 - }, - "timestamp": "2024-01-01T12:00:00Z" -} -``` - -### WebSocket 通信流程 - -```mermaid -sequenceDiagram - participant Client as Android 客户端 - participant WS as WebSocket 服务 - participant ASR as ASR 服务 - participant Agent as 对话 Agent - participant TTS as TTS 服务 - participant DB as 数据库 - - Client->>WS: 连接 WebSocket - Client->>WS: CONNECT 消息 - WS->>DB: 创建对话会话 - WS->>Client: 连接确认 - - loop 实时对话 - Client->>WS: AUDIO_CHUNK - WS->>ASR: 转文字 - ASR->>WS: 转写结果 - WS->>Client: TRANSCRIPT - WS->>Agent: 分析并生成回应 - Agent->>WS: 回应文本 - WS->>Client: AGENT_RESPONSE - WS->>TTS: 生成语音 - TTS->>WS: 音频数据 - WS->>Client: TTS_AUDIO - WS->>DB: 保存对话段落 - end - - Client->>WS: END_CONVERSATION - WS->>DB: 更新对话状态 - WS->>Agent: 触发整理 Agent - Agent->>DB: 生成章节 -``` - -### Android WebSocket 客户端实现 - -**文件:** `app-android/app/src/main/java/com/huaga/life_echo/network/WebSocketClient.kt` - -```kotlin -class WebSocketClient { - private val client = HttpClient { - install(WebSockets) - install(ContentNegotiation) { - json(Json { ignoreUnknownKeys = true }) - } - } - - suspend fun connect( - conversationId: String, - onMessage: (WebSocketMessage) -> Unit - ): WebSocketSession - - suspend fun sendAudioChunk(chunk: ByteArray) - suspend fun sendEndConversation() - suspend fun reconnect() -} -``` - -### 后端 WebSocket 端点实现 - -**文件:** `api/routers/websocket.py` - -```python -@app.websocket("/ws/conversation/{conversation_id}") -async def websocket_endpoint(websocket: WebSocket, conversation_id: str): - await websocket.accept() - try: - while True: - message = await websocket.receive_json() - message_type = message.get("type") - - if message_type == MessageType.AUDIO_CHUNK: - # 处理音频块 - transcript = await asr_service.transcribe(message["data"]["audio"]) - await websocket.send_json({ - "type": MessageType.TRANSCRIPT, - "data": {"text": transcript} - }) - - # Agent 生成回应 - response = await conversation_agent.generate_response(transcript) - await websocket.send_json({ - "type": MessageType.AGENT_RESPONSE, - "data": {"text": response} - }) - - # TTS 生成音频 - audio = await tts_service.synthesize(response) - await websocket.send_json({ - "type": MessageType.TTS_AUDIO, - "data": {"audio_base64": audio} - }) - - except WebSocketDisconnect: - # 处理断开连接 - pass -``` - ---- - -## 开发任务分解 - -### Android 端任务 - -#### 1. 项目配置 -- [ ] 配置 Ktor WebSocket 客户端依赖 -- [ ] 配置 Room 数据库依赖 -- [ ] 配置 Compose Navigation -- [ ] 配置权限处理(录音、存储) - -#### 2. 网络层 -- [ ] 实现 WebSocketClient(连接管理、消息发送/接收) -- [ ] 实现 WebSocketMessage 数据模型 -- [ ] 实现 REST API Service(章节查询、PDF 导出) -- [ ] 实现重连机制 - -#### 3. 数据层 -- [ ] 设计 Room 数据库表结构 -- [ ] 实现 Entity、DAO、Database -- [ ] 实现 Repository 层 -- [ ] 实现数据同步逻辑 - -#### 4. UI 层 -- [ ] 实现创建回忆录页面(实时对话界面) -- [ ] 实现我的回忆录页面(目录+章节阅读) -- [ ] 实现我的页面(用户设置) -- [ ] 实现导航和路由 - -#### 5. 功能模块 -- [ ] 实现语音录制模块(实时流式录制) -- [ ] 实现音频分块上传 -- [ ] 实现 TTS 音频播放 -- [ ] 实现主题配置(使用 color.png 配色) - -### 后端任务 - -#### 1. 项目配置 -- [ ] 配置 FastAPI WebSocket 支持 -- [ ] 配置 LangChain 环境 -- [ ] 配置 SQLAlchemy(SQLite) -- [ ] 配置 ASR/TTS 服务集成 - -#### 2. 数据库层 -- [ ] 设计数据库表结构 -- [ ] 实现 SQLAlchemy 模型 -- [ ] 实现数据库连接和会话管理 - -#### 3. Agent 实现 -- [ ] 创建访谈问题库(conversation_prompts.py) -- [ ] 实现对话 Agent(conversation_agent.py) -- [ ] 创建整理提示词模板(memory_prompts.py) -- [ ] 实现整理 Agent(memory_agent.py) - -#### 4. WebSocket 路由 -- [ ] 实现 WebSocket 端点(websocket.py) -- [ ] 实现消息处理逻辑 -- [ ] 实现连接管理 -- [ ] 实现心跳检测 - -#### 5. 服务层 -- [ ] 实现 ASR 服务(asr_service.py) -- [ ] 实现 TTS 服务(tts_service.py) -- [ ] 实现 PDF 生成服务(pdf_service.py) - -#### 6. REST API -- [ ] 实现章节查询接口(chapters.py) -- [ ] 实现回忆录接口(books.py) -- [ ] 实现用户认证接口 - ---- - -## 开发阶段规划 - -### 阶段 1:基础架构搭建(1-2周) - -**目标:** 搭建项目基础框架和开发环境 - -**Android:** -- 配置项目依赖(Ktor、Room、Compose) -- 创建基础项目结构 -- 实现基础导航和页面框架 -- 配置主题(color.png 配色) - -**后端:** -- 配置 FastAPI 和 WebSocket -- 配置 SQLite 数据库 -- 配置 LangChain 环境 -- 创建提示词模板文件结构 - -**交付物:** -- 可运行的基础项目框架 -- 数据库表结构设计文档 - -### 阶段 2:核心功能开发(2-3周) - -**目标:** 实现实时对话核心功能 - -**Android:** -- 实现 WebSocket 客户端 -- 实现语音录制和实时上传 -- 实现消息接收和处理 -- 实现对话页面 UI 交互 - -**后端:** -- 实现 WebSocket 端点 -- 集成 ASR 服务 -- 实现对话 Agent -- 集成 TTS 服务 -- 实现对话段落保存 - -**交付物:** -- 可用的实时对话功能 -- 完整的 WebSocket 通信流程 - -### 阶段 3:回忆录整理功能(1-2周) - -**目标:** 实现对话内容自动整理为章节 - -**后端:** -- 实现整理 Agent -- 实现章节生成和存储 -- 实现章节查询接口 - -**Android:** -- 实现回忆录阅读页面 -- 实现章节内容展示 -- 实现目录导航 - -**交付物:** -- 可用的章节整理功能 -- 可阅读的回忆录界面 - -### 阶段 4:导出和优化(1周) - -**目标:** 完善功能,优化用户体验 - -**后端:** -- 实现 PDF 生成服务 -- 优化 Agent 响应速度 -- 完善错误处理 - -**Android:** -- 实现 PDF 导出功能 -- 实现用户认证(微信登录) -- 实现设置页面 -- 优化 UI/UX - -**交付物:** -- 完整的 MVP 版本 -- 可导出的 PDF 功能 - ---- - -## 关键技术实现 - -### WebSocket 长连接管理 - -#### Android 端 -- 使用 Ktor WebSocket 客户端 -- 实现自动重连机制(指数退避) -- 处理网络中断和恢复 -- 实现心跳检测 - -#### 后端 -- 使用 FastAPI WebSocket 支持 -- 管理多个并发连接(连接池) -- 实现心跳检测 -- 优雅处理断开连接 - -### Agent 工作流编排 - -#### 对话 Agent -- 使用 LangChain ConversationBufferMemory 保存对话历史 -- 根据已聊话题动态选择下一个问题 -- 识别对话阶段转换 -- 优化提示词,减少响应延迟 - -#### 整理 Agent -- 使用 LangChain MapReduceDocumentsChain 处理长文本 -- 实现文本分类和章节归类 -- 批量处理对话段落 -- 异步执行,不阻塞用户操作 - -### 实时音频处理 - -- **音频分块传输**:每块 1-2 秒,避免单次传输过大 -- **流式 ASR**:实时转文字,低延迟 -- **TTS 音频流式返回**:边生成边返回,提升体验 - ---- - -## 文件清单 - -### Android 端新增文件 - -``` -app-android/app/src/main/java/com/huaga/life_echo/ -├── network/ -│ ├── WebSocketClient.kt # WebSocket 客户端 -│ ├── WebSocketMessage.kt # 消息数据模型 -│ └── ApiService.kt # REST API 接口 -├── data/ -│ ├── database/ -│ │ ├── AppDatabase.kt # Room 数据库 -│ │ └── dao/ # DAO 接口 -│ └── repository/ # Repository 实现 -├── ui/ -│ ├── screens/ -│ │ ├── CreateMemoryScreen.kt # 创建回忆录页面 -│ │ ├── MyMemoirScreen.kt # 我的回忆录页面 -│ │ └── ProfileScreen.kt # 我的页面 -│ └── navigation/ -│ └── AppNavigation.kt # 导航配置 -└── feature/ - └── voice/ - └── VoiceRecorder.kt # 语音录制模块 -``` - -### 后端新增文件 - -``` -api/ -├── main.py # FastAPI 应用入口 -├── agents/ -│ ├── conversation_agent.py # 对话 Agent -│ ├── memory_agent.py # 整理 Agent -│ └── prompts/ -│ ├── conversation_prompts.py # 访谈问题库 -│ └── memory_prompts.py # 整理提示词 -├── database/ -│ ├── models.py # SQLAlchemy 模型 -│ └── database.py # 数据库连接 -├── routers/ -│ ├── websocket.py # WebSocket 端点 -│ ├── chapters.py # 章节接口 -│ └── books.py # 回忆录接口 -└── services/ - ├── asr_service.py # ASR 服务 - ├── tts_service.py # TTS 服务 - └── pdf_service.py # PDF 生成服务 -``` - ---- - -## 注意事项 - -### 1. 配色方案 -- 严格按照 `docs/color.png` 的配色设计 UI -- 保持界面简洁、优雅 - -### 2. 数据隐私 -- 用户数据默认仅自己可见 -- 实现数据加密存储 -- 遵守隐私保护法规 - -### 3. 实时性优化 -- WebSocket 连接要保持稳定 -- 实现完善的重连机制 -- 优化 Agent 响应速度(提示词优化、模型选择) - -### 4. 音频处理 -- 音频分块大小要合适(平衡实时性和网络负载) -- 支持多种音频格式 -- 处理网络波动情况 - -### 5. 章节整理 -- 对话结束后异步触发,不阻塞用户操作 -- 支持增量更新章节 -- 提供重新整理功能 - -### 6. 错误处理 -- 完善的错误提示和重试机制 -- 记录错误日志,便于排查 -- 优雅降级(网络中断时的处理) - -### 7. 性能优化 -- 数据库查询优化(索引) -- 音频文件压缩和缓存 -- UI 渲染性能优化 - ---- - -## 附录 - -### 参考文档 -- [AGENT.md](AGENT.md) - 产品需求文档 -- [人生故事访谈问题清单(轻松对话版).pdf] - 访谈问题参考 -- [名人传记结构与回忆录访谈提问模板研究报告.pdf] - 传记结构参考 - -### 技术文档 -- [Ktor WebSocket 文档](https://ktor.io/docs/websocket-client.html) -- [LangChain 文档](https://python.langchain.com/) -- [FastAPI WebSocket 文档](https://fastapi.tiangolo.com/advanced/websockets/) - ---- - -**最后更新:** 2024-01-07 diff --git a/docs/数据库设计.md b/docs/数据库设计.md deleted file mode 100644 index a828167..0000000 --- a/docs/数据库设计.md +++ /dev/null @@ -1,123 +0,0 @@ -# 数据库设计文档 - -## Android 本地数据库(Room) - -### User 表 -```kotlin -@Entity(tableName = "users") -data class User( - @PrimaryKey val id: String, - val nickname: String, - val avatarUrl: String?, - val subscriptionType: String, // free, premium - val createdAt: Long -) -``` - -### Conversation 表 -```kotlin -@Entity(tableName = "conversations") -data class Conversation( - @PrimaryKey val id: String, - val userId: String, - val startedAt: Long, - val endedAt: Long?, - val durationSeconds: Int, - val summary: String?, - val currentTopic: String?, // 当前话题 - val conversationStage: String? // CHILDHOOD, EDUCATION, CAREER, FAMILY, BELIEFS, SUMMARY -) -``` - -### ConversationSegment 表 -```kotlin -@Entity(tableName = "conversation_segments") -data class ConversationSegment( - @PrimaryKey val id: String, - val conversationId: String, - val audioPath: String?, - val transcriptText: String, - val createdAt: Long, - val processed: Boolean, - val topicCategory: String? // 话题分类 -) -``` - -### Chapter 表 -```kotlin -@Entity(tableName = "chapters") -data class Chapter( - @PrimaryKey val id: String, - val title: String, - val content: String, - val orderIndex: Int, - val status: String, // draft, completed - val updatedAt: Long, - val category: String // 章节分类 -) -``` - -### Book 表 -```kotlin -@Entity(tableName = "books") -data class Book( - @PrimaryKey val id: String, - val userId: String, - val title: String, - val totalPages: Int, - val totalWords: Int, - val updatedAt: Long -) -``` - -## 后端数据库(SQLAlchemy) - -### users 表 -- id (String, Primary Key) -- openid (String, Unique) - 微信 OpenID -- nickname (String) -- avatar_url (String, Nullable) -- subscription_type (String) - free, premium -- created_at (DateTime) - -### conversations 表 -- id (String, Primary Key) -- user_id (String, Foreign Key -> users.id) -- started_at (DateTime) -- ended_at (DateTime, Nullable) -- duration_seconds (Integer) -- summary (String, Nullable) -- status (String) - active, ended, processing -- current_topic (String, Nullable) -- conversation_stage (String, Nullable) - childhood, education, career, family, beliefs, summary - -### segments 表 -- id (String, Primary Key) -- conversation_id (String, Foreign Key -> conversations.id) -- audio_url (String, Nullable) -- transcript_text (Text) -- created_at (DateTime) -- processed (Boolean, Default False) -- topic_category (String, Nullable) -- agent_response (Text, Nullable) - -### chapters 表 -- id (String, Primary Key) -- user_id (String, Foreign Key -> users.id) -- title (String) -- content (Text) -- order_index (Integer) -- status (String) - draft, completed -- images (JSON, Nullable) - 图片 URL 列表 -- updated_at (DateTime) -- category (String) - 章节分类 - -### books 表 -- id (String, Primary Key) -- user_id (String, Foreign Key -> users.id) -- title (String) -- total_pages (Integer) -- total_words (Integer) -- cover_image_url (String, Nullable) -- updated_at (DateTime) - diff --git a/skills/README.md b/skills/README.md deleted file mode 100644 index 798547f..0000000 --- a/skills/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# 项目通用技术设计(Skills) - -本目录描述与本项目**具体业务无关**的技术设计与约定,供新功能开发、重构或跨项目复用时参考。 - -## Skill 列表 - -| Skill | 说明 | -|-------|------| -| [auth-login-register.md](auth-login-register.md) | 登录与注册机制(服务端 API + 客户端 Android) | -| [top-app-bar.md](top-app-bar.md) | 顶部导航栏(App Bar)与透明化 | -| [system-bars.md](system-bars.md) | 系统状态栏与系统导航栏 | -| [bottom-navigation.md](bottom-navigation.md) | 底部导航栏(Tab 栏) | -| [android-navigation-routing.md](android-navigation-routing.md) | Android 路由与页面导航 | -| [error-handling.md](error-handling.md) | 错误处理(Android 友好错误展示) | -| [api-backend-conventions.md](api-backend-conventions.md) | API 与后端约定 | - -## 使用约定 - -- 新增与「登录注册、导航栏、系统栏、路由、错误展示」相关的功能时,优先符合对应 Skill 中的设计,保持体验一致。 -- 复用到其他项目时,可按需裁剪(例如只保留认证 + 顶部栏 + 错误处理),并同步调整客户端与服务端约定。 -- 业务相关逻辑(如回忆录、对话、付费等)不写入本目录,仅保留通用技术部分。 diff --git a/skills/android-navigation-routing.md b/skills/android-navigation-routing.md deleted file mode 100644 index d2c196b..0000000 --- a/skills/android-navigation-routing.md +++ /dev/null @@ -1,24 +0,0 @@ -# Skill:Android 路由与页面导航 - -与具体业务无关的 Android 路由与 NavHost 设计:路由定义、转场与返回栈。 - ---- - -## 路由定义 - -- 使用 **Screen** sealed class,所有路由字符串集中定义(如 `Screen.ConversationList.route`、`Screen.CreateMemory.createRoute(conversationId)` 等)。 - -## NavHost - -- 在 **AppNavigation** 中统一注册 `composable(route = ..., enterTransition = ..., exitTransition = ..., popEnterTransition = ..., popExitTransition = ...)`。 -- 转场动画由 **NavigationTransitions** 提供:水平进出(如 `slideInHorizontally`、`slideOutHorizontally`)、从左侧返回(`slideInHorizontallyFromLeft`、`slideOutHorizontallyToRight`)、淡入淡出、缩放等,按页面类型选用。 - -## 起始目的地 - -- `startDestination = if (isLoggedIn) Screen.ConversationList.route else Screen.Login.route`,与 TokenManager 的登录状态一致。 - -## 返回栈 - -- 登录后进入主界面:`popUpTo(Screen.Login.route) { inclusive = true }`。 -- 登出:`popUpTo(0) { inclusive = true }` 清空栈再 navigate 到 Login。 -- Tab 切换时按设计选择 `popUpTo(ConversationList)` 等,避免栈过深。 diff --git a/skills/api-backend-conventions.md b/skills/api-backend-conventions.md deleted file mode 100644 index 913025e..0000000 --- a/skills/api-backend-conventions.md +++ /dev/null @@ -1,18 +0,0 @@ -# Skill:API 与后端约定 - -与具体业务无关的后端 API 与配置约定:前缀、敏感信息与数据库。 - ---- - -## 前缀与路由 - -- 认证:`/api/auth`。 -- 其他业务按模块分 router(如 user、conversations、books、chapters 等),在 `main.py` 中挂载,统一带 `/api` 前缀时在挂载处配置。 - -## 敏感配置 - -- 环境变量中敏感项(SECRET_KEY、API_KEY、PASSWORD、TOKEN 等)在日志中脱敏(只打 key 或前后几位)。 - -## 数据库 - -- 异步 SQLAlchemy + `get_async_db`;需当前用户时 `Depends(get_current_user)`。 diff --git a/skills/auth-login-register.md b/skills/auth-login-register.md deleted file mode 100644 index 1444ff8..0000000 --- a/skills/auth-login-register.md +++ /dev/null @@ -1,62 +0,0 @@ -# Skill:登录与注册机制 - -与具体业务无关的认证设计:服务端 API 与客户端 Android 的登录、注册、令牌与鉴权约定。 - ---- - -## 服务端(API) - -### 认证方式 - -- **访问令牌(Access Token)**:JWT,用于接口鉴权,默认有效期 2 小时(`ACCESS_TOKEN_EXPIRE_MINUTES`)。 -- **刷新令牌(Refresh Token)**:随机字符串,存库,默认 30 天有效;用于在访问令牌过期后换取新访问令牌。 - -### 密码 - -- 使用 **bcrypt** 哈希存储,不存明文。 -- 注册/登录时由 `auth_service.hash_password` / `verify_password` 处理。 - -### 登录/注册入口 - -- 手机号 + 密码:`POST /api/auth/login`、`POST /api/auth/register`。 -- 手机号 + 短信验证码:`POST /api/auth/login/sms`(未注册则自动注册,需提供昵称)。 -- 所有入口均要求请求体中 `agreed_to_terms == true`,否则 400。 - -### 令牌刷新 - -- `POST /api/auth/refresh`,Body: `{ "refresh_token": "..." }`。 -- 校验:存在、未撤销、未过期、对应用户存在;通过后返回新的 `access_token`,`refresh_token` 原样返回(不轮换)。 - -### 鉴权依赖 - -- 需要登录的接口:`Depends(get_current_user)`,从 `Authorization: Bearer ` 解析 JWT,校验 `type == "access"` 并加载用户。 -- 可选登录:`Depends(get_optional_user)`,无 token 或无效时返回 `None`。 - -### 路由与安全 - -- 认证相关路由统一前缀:`/api/auth`(如 login、register、refresh、logout 等)。 -- OAuth2 约定:`OAuth2PasswordBearer(tokenUrl="/api/auth/login")`,仅用于声明从哪里取 token,实际登录仍用上述接口。 - ---- - -## 客户端(Android) - -### 令牌存储 - -- 使用 **DataStore Preferences**(`TokenPreferences`)存 `access_token`、`refresh_token`、`user_id`。 -- 通过单例 **TokenManager** 读写;提供 `initialize(context)`、`saveTokens`、`getAccessToken`/`getRefreshToken`(含 suspend 与 sync 版本)、`clearTokens`。 - -### 登录状态 - -- TokenManager 维护 `isLoggedIn`(有有效 access_token 即为 true),并提供 `rememberIsLoggedIn()` 供 Compose 使用。 -- 登出或刷新失败时调用 `clearTokens()` 并 `notifyTokenRefreshFailed()`,由回调统一处理(如清栈并跳转登录页)。 - -### 请求头与刷新 - -- 使用 **AuthInterceptor**(Ktor Client 插件):请求前自动附加 `Authorization: Bearer `。 -- 收到 401 时:用当前 refresh_token 调 `AuthService.refreshToken()`;成功则写回新 token,失败则 `clearTokens()` + `notifyTokenRefreshFailed()`,由 App 层跳转登录。 - -### 登录成功/登出与导航 - -- 登录成功:由各登录入口回调 `onLoginSuccess`,主流程将 `isLoggedIn = true` 并导航到主界面(如会话列表),并 `popUpTo(Login)` 清掉登录页。 -- 登出或刷新失败:`onLogout` / TokenManager 回调中执行 `navController.navigate(Screen.Login.route) { popUpTo(0) { inclusive = true } }`,保证回到登录且无法返回。 diff --git a/skills/bottom-navigation.md b/skills/bottom-navigation.md deleted file mode 100644 index 7ebecdb..0000000 --- a/skills/bottom-navigation.md +++ /dev/null @@ -1,20 +0,0 @@ -# Skill:底部导航栏(Tab 栏) - -与具体业务无关的底部 Tab 栏设计:显示条件、实现方式与选中态。 - ---- - -## 显示条件 - -- 仅在部分主 Tab 页显示:如会话列表、回忆录、我的(以 `Screen.ConversationList`、`Screen.MyMemoir`、`Screen.Profile` 等当前路由判断)。 -- 子页(如设置、关于、FAQ)不显示底部栏。 - -## 实现 - -- 在 `Scaffold.bottomBar` 中按 `currentRoute` 条件渲染底部栏;使用 `Surface` + `windowInsetsPadding(WindowInsets.navigationBars)` 避免与系统导航栏重叠。 -- 底部项使用 **AppDestinations** 枚举(如 CHAT、MEMOIR、PROFILE),每项含 label、icon;点击时 `navController.navigate(对应 Screen.route)` 并配合 `popUpTo` 控制返回栈。 - -## 选中态 - -- 通过 `currentDestination` 与当前路由同步(`LaunchedEffect(currentRoute)`),保证高亮与实际页面一致。 -- 可选:选中缩放/按下缩放等动画(如 `BottomNavItem` 中的 `animateFloatAsState`)。 diff --git a/skills/error-handling.md b/skills/error-handling.md deleted file mode 100644 index 6e0fb26..0000000 --- a/skills/error-handling.md +++ /dev/null @@ -1,27 +0,0 @@ -# Skill:错误处理(Android) - -与具体业务无关的 Android 错误展示设计:错误类型、友好文案与组件。 - ---- - -## 错误类型 - -- **ErrorType** 枚举:NETWORK、SERVER、TIMEOUT、AUTH、NOT_FOUND、VALIDATION、UNKNOWN。 - -## ErrorHandler - -- `getFriendlyError(errorType, originalMessage)`:生产环境返回友好文案,开发环境可带出 `originalMessage`(如 VALIDATION)。 -- `detectErrorType(exception)`:根据异常 message 关键词推断类型。 -- `detectErrorTypeByStatusCode(statusCode)`:401/403→AUTH,404→NOT_FOUND,5xx→SERVER 等。 -- `getDisplayMessage` / `handleException`:统一得到最终展示文案。 - -## 组件 - -- **FriendlyErrorView**:全屏错误页,图标+标题+描述+重试/去登录按钮,可选动画。 -- **FriendlyErrorDialog**:弹窗版,带确认/取消或重试。 -- **InlineErrorMessage**:表单等内联错误,生产环境对部分技术性文案做友好替换。 -- **Snackbar**:`showFriendlyError` 扩展,用于轻量提示。 - -## 约定 - -- 生产环境不直接暴露后端异常原文;开发环境可通过 `AppConfig.isDebugMode` 显示原始信息。 diff --git a/skills/system-bars.md b/skills/system-bars.md deleted file mode 100644 index 3504e51..0000000 --- a/skills/system-bars.md +++ /dev/null @@ -1,19 +0,0 @@ -# Skill:系统状态栏与系统导航栏 - -与具体业务无关的系统栏设计:边缘到边缘、显示/隐藏与外观。 - ---- - -## 边缘到边缘 - -- `MainActivity` 中 `enableEdgeToEdge()`,`WindowCompat.setDecorFitsSystemWindows(window, false)`,内容可延伸到系统栏下。 - -## 显示/隐藏 - -- 使用 **SystemUiController** Composable:`LaunchedEffect` 内通过 `WindowInsetsController` 的 `show`/`hide` 控制 `statusBars()`、`navigationBars()`。 -- 本项目主界面常将状态栏与导航栏隐藏(`isStatusBarVisible = false`, `isNavigationBarVisible = false`),由自定义顶部栏和底部栏替代。 - -## 行为与外观 - -- `systemBarsBehavior = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE`:隐藏时仍可从边缘滑出临时显示。 -- 图标颜色随主题:`isAppearanceLightStatusBars = !darkMode`,亮色主题用深色图标,暗色主题用浅色图标。 diff --git a/skills/top-app-bar.md b/skills/top-app-bar.md deleted file mode 100644 index 4f8e224..0000000 --- a/skills/top-app-bar.md +++ /dev/null @@ -1,23 +0,0 @@ -# Skill:顶部导航栏(App Bar) - -与具体业务无关的顶部导航栏设计:透明化、状态栏占位与使用约定。 - ---- - -## 透明化 - -- 使用 **TransparentTopAppBar**(Material3 TopAppBar 封装),支持三种模式: - - `FULLY_TRANSPARENT`:背景完全透明。 - - `SEMI_TRANSPARENT`:半透明,可设 `alpha`。 - - `GRADIENT`:自上而下渐变透明(常用)。 -- 已处理状态栏占位:`windowInsetsPadding(WindowInsets.statusBars)`。 -- 标题/返回/图标颜色需根据背景选:浅底用 `onSurface`,深色底用浅色,以保证可读性。 - -## 使用约定 - -- 普通页:在 `Scaffold.topBar` 中放 `TransparentTopAppBar`,按需选 `transparencyType`、`gradientColors`/`alpha`。 -- 聊天页等:可用 **ChatHeader**,通过 `isTransparent`、`transparencyType`、`alpha` 控制透明效果。 - -## 文档 - -- 详细用法见:`app-android/doc/透明化导航栏使用指南.md`。