From c45a2c040b68346a40bbb9085d90b2d0bc3b5f00 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 13 May 2026 15:01:50 +0800 Subject: [PATCH] =?UTF-8?q?fix(expo):=20=E6=9A=82=E5=81=9C=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E6=9C=97=E8=AF=BB=E5=90=8E=E7=BB=A7=E7=BB=AD=E6=92=AD?= =?UTF-8?q?=E6=94=BE=E6=9C=80=E6=96=B0=20TTS=20=E7=89=87=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - usePlayer:paused 且 tts_auto 时清空队列并重置,再播当前片段 - 用 statusRef 与暂停同步,避免 WS 紧连 enqueue 时状态滞后 - 补充 use-player 单测 - api: 调整 copyright_source_pdf 脚本 - docs: 新增软著《岁月时书》软件设计说明书 Co-authored-by: Cursor --- api/scripts/copyright_source_pdf.py | 6 +- .../src/features/voice/hooks/use-player.ts | 36 +- .../tests/features/voice/use-player.test.tsx | 40 ++ docs/软著-岁月时书-软件设计说明书.md | 520 ++++++++++++++++++ 4 files changed, 598 insertions(+), 4 deletions(-) create mode 100644 docs/软著-岁月时书-软件设计说明书.md diff --git a/api/scripts/copyright_source_pdf.py b/api/scripts/copyright_source_pdf.py index 68d6829..7ff17f1 100644 --- a/api/scripts/copyright_source_pdf.py +++ b/api/scripts/copyright_source_pdf.py @@ -52,9 +52,9 @@ VERSION = "V1.0.0" SOURCE_ROOT = REPO_ROOT OUTPUT_PDF = REPO_ROOT / "copyright_source_listing.pdf" -# PDF 页眉/材料中出现的文件路径:将本机目录前缀替换为申报用虚拟根(如 heguangtongkun/life-echo/...) +# PDF 页眉/材料中出现的文件路径:将本机目录前缀替换为申报用单位根(如 上海华嘎科技有限公司/life-echo/...) FILE_PATH_ABSOLUTE_PREFIX = Path("/Users/kevin/Codes/hgtk") -FILE_PATH_DISPLAY_ROOT = "heguangtongkun" +FILE_PATH_DISPLAY_ROOT = "上海华嘎科技有限公司" # 设为 None 时自动探测(macOS:Menlo.ttc / Songti.ttc 等);也可填本机 TTF/OTF 或 TTC 路径 CJK_MONO_FONT_PATH: Path | None = None @@ -330,7 +330,7 @@ def iter_source_files(root: Path) -> Iterable[Path]: def path_for_pdf_listing(fp: Path) -> str: - """将绝对路径转为 PDF 展示路径:/Users/.../hgtk/foo -> heguangtongkun/foo。""" + """将绝对路径转为 PDF 展示路径:.../hgtk/foo -> 上海华嘎科技有限公司/foo。""" try: resolved = fp.resolve() base = FILE_PATH_ABSOLUTE_PREFIX.expanduser().resolve() diff --git a/app-expo/src/features/voice/hooks/use-player.ts b/app-expo/src/features/voice/hooks/use-player.ts index 1c2c7c7..57380b5 100644 --- a/app-expo/src/features/voice/hooks/use-player.ts +++ b/app-expo/src/features/voice/hooks/use-player.ts @@ -40,6 +40,8 @@ export function usePlayer(): UsePlayerResult { const isPlayingRef = useRef(false); const wasBlockedByRecorderRef = useRef(false); const isPlayNextInProgressRef = useRef(false); + /** 与 `status` 同步;`pausePlayback` 等在同一事件循环内立即更新,避免 WS 紧跟着 `enqueue(tts_auto)` 时读到陈旧 `playing`。 */ + const statusRef = useRef('idle'); /** 同步反映「当前是否正在播放某条 URI」;enqueue 不能依赖 state,否则 await stop() 后仍为陈旧闭包。 */ const playbackActiveUriRef = useRef(null); /** 当前 source 是否已进入过 playing=true,避免换源瞬间 playerStatus 仍带上一首的 duration 而误判「已播完」。 */ @@ -63,6 +65,10 @@ export function usePlayer(): UsePlayerResult { const player = useAudioPlayer(currentSource, playerOptions); const playerStatus = useAudioPlayerStatus(player); + useEffect(() => { + statusRef.current = status; + }, [status]); + /** * 必须在 `isLoaded` 之后再 `play()`。 * expo-audio 在 `downloadFirst: true` 时先用 null 建 player,再在内部 effect 里异步 @@ -86,6 +92,7 @@ export function usePlayer(): UsePlayerResult { playbackActiveUriRef.current = null; setCurrentPlaybackItem(null); setCurrentSource(null); + statusRef.current = 'idle'; setStatus('idle'); setQueueLength(0); await audioFocus.releaseIfOwnedBy('player'); @@ -100,6 +107,7 @@ export function usePlayer(): UsePlayerResult { * `wasBlockedByRecorderRef` 不会被置位,录音结束后也不会重试 playNext。 */ wasBlockedByRecorderRef.current = true; + statusRef.current = 'idle'; setStatus('idle'); return; } @@ -108,6 +116,7 @@ export function usePlayer(): UsePlayerResult { const next = queueRef.current.shift()!; setQueueLength(queueRef.current.length); + statusRef.current = 'playing'; setStatus('playing'); trackHasPlayedRef.current = false; playbackActiveUriRef.current = next.uri; @@ -150,6 +159,7 @@ export function usePlayer(): UsePlayerResult { player.pause(); } isPlayingRef.current = false; + statusRef.current = 'paused'; return 'paused'; }); }, [player]); @@ -158,12 +168,14 @@ export function usePlayer(): UsePlayerResult { if (status !== 'paused') return; const acquired = await audioFocus.acquireForPlayback(); if (!acquired) { + statusRef.current = 'idle'; setStatus('idle'); return; } if (!player) return; if (!playerStatus.isLoaded) return; player.play(); + statusRef.current = 'playing'; setStatus('playing'); isPlayingRef.current = true; }, [status, player, playerStatus.isLoaded]); @@ -192,6 +204,26 @@ export function usePlayer(): UsePlayerResult { const enqueue = useCallback( async (item: PlaybackItem) => { + /** + * 用户在助手自动朗读中途点暂停时,`playbackActiveUriRef` 仍指向当前条, + * 后续 `tts_auto` 只会堆在队列里且不会 `playNext`。 + * 新片段到达时表示「最新已生成」:清掉暂停态与积压队列,只播本条。 + */ + if (item.kind === 'tts_auto' && statusRef.current === 'paused') { + queueRef.current = []; + setQueueLength(0); + isPlayingRef.current = false; + if (player) { + player.pause(); + } + playbackActiveUriRef.current = null; + setCurrentPlaybackItem(null); + setCurrentSource(null); + statusRef.current = 'idle'; + setStatus('idle'); + await audioFocus.releaseIfOwnedBy('player'); + } + queueRef.current.push(item); setQueueLength(queueRef.current.length); @@ -202,7 +234,7 @@ export function usePlayer(): UsePlayerResult { await playNext(); } }, - [playNext], + [playNext, player], ); const enqueueExclusive = useCallback( @@ -216,6 +248,7 @@ export function usePlayer(): UsePlayerResult { playbackActiveUriRef.current = null; setCurrentPlaybackItem(null); setCurrentSource(null); + statusRef.current = 'idle'; setStatus('idle'); await audioFocus.releaseIfOwnedBy('player'); await playNext(); @@ -235,6 +268,7 @@ export function usePlayer(): UsePlayerResult { playbackActiveUriRef.current = null; setCurrentPlaybackItem(null); setCurrentSource(null); + statusRef.current = 'idle'; setStatus('idle'); await audioFocus.releaseIfOwnedBy('player'); }, [player]); diff --git a/app-expo/tests/features/voice/use-player.test.tsx b/app-expo/tests/features/voice/use-player.test.tsx index fe5e47a..0bdd40d 100644 --- a/app-expo/tests/features/voice/use-player.test.tsx +++ b/app-expo/tests/features/voice/use-player.test.tsx @@ -167,4 +167,44 @@ describe('usePlayer', () => { expect(acquire).toHaveBeenCalledTimes(2); expect(play).toHaveBeenCalled(); }); + + test('after pause, new tts_auto clears backlog and kicks playNext', async () => { + mockUseAudioPlayerStatus.mockReturnValue({ + isLoaded: true, + playing: false, + currentTime: 0.1, + duration: 10, + }); + const pause = jest.fn(); + const play = jest.fn(); + mockUseAudioPlayer.mockReturnValue({ pause, play }); + + const { result } = renderHook(() => usePlayer()); + + await act(async () => { + await result.current.enqueue({ + uri: 'file:///first.mp3', + kind: 'tts_auto', + }); + }); + + expect(result.current.status).toBe('playing'); + const playCountAfterFirst = play.mock.calls.length; + + act(() => { + result.current.pausePlayback(); + }); + expect(result.current.status).toBe('paused'); + + await act(async () => { + await result.current.enqueue({ + uri: 'file:///latest.mp3', + kind: 'tts_auto', + }); + }); + + expect(result.current.status).toBe('playing'); + expect(play.mock.calls.length).toBeGreaterThan(playCountAfterFirst); + expect(result.current.currentSource).toBe('file:///latest.mp3'); + }); }); diff --git a/docs/软著-岁月时书-软件设计说明书.md b/docs/软著-岁月时书-软件设计说明书.md new file mode 100644 index 0000000..2c1d17a --- /dev/null +++ b/docs/软著-岁月时书-软件设计说明书.md @@ -0,0 +1,520 @@ +# 软件设计说明书(兼操作手册) + +--- + +## 文档封面信息 + + +| 项目 | 内容 | +| ------ | ------------- | +| 软件全称 | 岁月留书 | +| 英文名称 | Life Echo | +| 文档名称 | 《岁月留书》软件设计说明书 | +| 文档版本 | 1.0 | +| 对应软件版本 | 1.0 | +| 著作权人 | 上海华嘎科技有限公司 | +| 编写单位 | 上海华嘎科技有限公司 | +| 编写日期 | 2026年5月12日 | + + +--- + +## 第 1 章 引言 + +### 1.1 编写目的 + +本说明书描述「岁月留书(Life Echo)」一体化软件系统在代码仓库层面的组成结构、总体架构、主要功能规格、核心业务流程、开发与测试状况以及典型环境下的安装与运维使用方法。 + +### 1.2 适用范围 + +本说明书适用于以下组成部分的整体说明: + +1. **后端 API 服务**:路径 `api/`,基于 FastAPI,对外提供 REST API、WebSocket 实时对话能力及静态资源挂载。 +2. **客户端应用**:路径 `app-expo/`,基于 Expo Router 与 React Native,面向终端用户提供登录、对话、回忆录浏览与导出相关交互界面。 +3. **内部评测前端**:路径 `app-eval-web/`,基于 Vite,用于开发与回归评测场景(一般不纳入终端用户交付镜像,与内部评测 API 配合使用)。 +4. **工程与运维文档**:路径 `docs/`,存放与本项目相关的运维、设计备忘等资料;本说明书为其中面向著作权登记的汇总文档。 + +### 1.3 术语与缩写 + + +| 术语 | 含义 | +| ------------------ | ----------------------------------------------------- | +| JWT | JSON Web Token,访问令牌与刷新令牌组合的认证机制 | +| WebSocket | 全双工通信协议,本项目中用于实时语音与文本对话管线 | +| ASR | Automatic Speech Recognition,语音转文字 | +| TTS | Text-To-Speech,文字转语音 | +| LLM | Large Language Model,大语言模型;后端通过适配层调用兼容 OpenAI API 的服务 | +| Agent | 基于 LangChain 组织的对话与回忆录编排逻辑单元 | +| ChatOrchestrator | 实时对话编排入口,负责将用户输入路由至画像采集或访谈对话等分支 | +| MemoirOrchestrator | 回忆录正文流水线编排入口,负责分段调度抽取、分类与各 Specialist Agent | +| Celery | Python 异步分布式任务队列,用于回忆录 Phase 处理、记忆压实、嵌入调度等后台作业 | +| Phase1 / Phase2 | 回忆录批量处理阶段的工程称谓:Phase1 侧重批次准备与派发,Phase2 按类别执行故事管线 | +| Redis | 内存数据结构存储,用作会话缓存与队列协调等 | +| PostgreSQL | 关系型数据库;会话消息等持久化真源存放于此 | +| pgvector | PostgreSQL 扩展,用于向量检索相关的记忆块存储与检索 | +| Alembic | 数据库 Schema 迁移工具;本项目禁止手写 DDL 绕过迁移脚本 | + + +--- + +## 第 2 章 系统概述 + +### 2.1 建设目标与业务边界 + +本软件面向「口述回忆录」类产品形态,建设目标包括: + +1. **实时智能访谈**:通过 WebSocket 维持长连接,在移动端采集用户语音或文本输入,经 ASR(若语音)形成可处理文本,再由 LLM 驱动的 Agent 结合传记结构与历史记忆检索结果生成引导性回复。 +2. **结构化回忆录生成**:将多轮对话中的有效叙事内容抽取、归类,按章节与阅读片段组织为可阅读的「回忆录」文稿,并支持章节级素材与图像任务(工程上区分为正文配图与章节封面等管线)。 +3. **可导出成品**:服务端将回忆录内容排版并生成 PDF 等可供用户留存与分享的文档形式(具体能力以当前 `features/memoir` 与相关服务实现为准)。 +4. **账户、套餐与商业化支撑**:提供用户注册登录、套餐(Plan)、配额(Quota)、订单与支付(含微信、支付宝等适配)等能力,保证多租户下的资源可控与可审计。 + +### 2.2 非功能需求概要 + + +| 类别 | 要求说明 | +| ----- | ----------------------------------------------------------------- | +| 实时性 | WebSocket 连接内消息顺序稳定;首轮「边播 TTS 边出字」模式下,服务端按协议先推送音频再推送对应文本分段 | +| 安全 | REST 接口使用 JWT;WebSocket 连接通过 Query 携带 `access_token` 校验身份 | +| 可运维 | 健康检查路由 `/health`;请求追踪依赖中间件注入 request id;日志采用统一结构化封装 | +| 可扩展 | 外部厂商能力集中在 `adapters`,业务仅依赖 `ports` 协议,便于替换 ASR/TTS/LLM/短信/对象存储等实现 | +| 数据一致性 | 会话消息以数据库表 `conversation_messages` 为权威存储,Redis 承担缓存与加速读取职责 | + + +### 2.3 系统边界图(逻辑) + +客户端(Expo 应用或评测前端)仅与本后端 HTTP/WebSocket 交互;后端再访问 PostgreSQL、Redis,并向 Celery Worker 投递异步任务;外部云服务包括 LLM、ASR、TTS、短信、对象存储及支付网关等,经由适配层调用。 + +--- + +## 第 3 章 总体设计与运行环境 + +### 3.1 Monorepo 目录组成 + +仓库根目录主要包含: + +``` +life-echo/ +├── api/ # 后端(FastAPI,Python,uv + Alembic + Celery) +├── app-expo/ # 移动端(Expo Router + React Native) +├── app-eval-web/ # 内部评测 Web(Vite) +├── docs/ # 设计与运维文档(含本说明书) +├── package.json # 根级脚本聚合(如 husky、子项目脚本转发) +└── README.md # 项目总览 +``` + +### 3.2 后端进程形态 + +1. **主 API 进程**:模块入口 `api/app/main.py` 创建 FastAPI 应用 `app`,挂载各业务 Router,注册 WebSocket 路径 `/ws/conversation/{conversation_id}`,并在启动阶段执行 Alembic 迁移、Redis 连接、ASR 就绪检查、微信支付客户端预初始化等。 +2. **内部评测 API 进程**(可选隔离部署):模块入口 `api/app/internal_main.py` 创建 `internal_app`,挂载 `evaluation` 相关路由,用于回归评测与会话对比,默认与主 API 进程分离以降低评测对线上入口的影响。 +3. **Celery Worker**:消费 memoir 处理、记忆压实、嵌入调度、记忆富化等任务队列;具体队列名与路由以 `tasks` 包及环境变量为准。 + +### 3.3 客户端组成 + +`app-expo` 采用文件式路由,源码主目录为 `src/app/`,典型路由分组包括: + + +| 路由分组 | 相对路径示例 | 用途说明 | +| ------- | --------------------------------------------------------------------------------------------------- | ------------------------- | +| 认证 | `(auth)/login.tsx`、`register.tsx`、`reset-password.tsx` | 登录、注册、重置密码 | +| 主导航 Tab | `(tabs)/index.tsx`、`memoir.tsx`、`profile.tsx` | 首页、回忆录、个人中心 | +| 对话与内容 | `(main)/conversation/[id].tsx`、`chapter/[id].tsx` | 单会话对话页、章节阅读 | +| 设置与合规 | `(main)/personal-info.tsx`、`export-data.tsx`、`delete-data.tsx`、`faq.tsx`、`feedback.tsx`、`about.tsx` | 个人信息、导入导出与删除数据、常见问题、反馈、关于 | +| 法律文档 | `legal/[type].tsx` | 法律类展示页 | + + +### 3.4 开发与运行依赖(摘要) + +后端建议环境:Python 3.10 及以上;依赖与锁文件由 `uv` 管理(`api/pyproject.toml` 与 `api/uv.lock`)。数据库使用 PostgreSQL;缓存与消息代理使用 Redis。容器编排可使用 `docker-compose` 系列文件(以 `api/` 目录下实际文件名为准)。 + +移动端使用 Node.js 生态执行 `npm install` 与 `npx expo start` 等命令。内部评测前端在 `app-eval-web/` 目录使用 Vite 开发服务器。 + +### 3.5 内部评测前端(app-eval-web) + +`app-eval-web` 为研发与质量保障使用的单页应用工程,默认仅监听本机开发端口(常见为 `5174`,以 Vite 控制台输出为准)。其通过 HTTP 调用 `internal_app` 暴露的评测 REST API,前缀固定为 `/internal/api/evaluation`(见 `api/app/internal_main.py` 中的 `include_router` 配置)。该前端**不作为**终端用户应用商店交付物的一部分描述,仅在说明「Monorepo 完整组成」时列出。 + +--- + +## 第 4 章 软件结构说明 + +### 4.1 后端分层架构 + +后端严格按功能域拆分,并遵守「路由不写业务 SQL、服务不直连三方 SDK」的边界约定: + + +| 层次 | 目录 | 职责 | +| ---- | --------------------------- | -------------------------------------------------------------- | +| 接口层 | `app/features/*/router.py` | 定义 REST 或协议入口,注入服务依赖,返回 Pydantic 模型 | +| 业务层 | `app/features/*/service.py` | 业务规则、事务边界、编排对多个 repo 与端口的调用 | +| 持久层 | `app/features/*/repo.py` | 数据库增删查改,不在此层随意 `commit`(由上层统一事务策略) | +| 领域模型 | `app/features/*/models.py` | SQLAlchemy ORM 模型 | +| 端口 | `app/ports/` | 协议(Protocol),描述 ASR、TTS、LLM、短信等能力 | +| 适配器 | `app/adapters/` | 具体厂商实现,如 DeepSeek、腾讯、OpenAI Whisper 等 | +| 智能体 | `app/agents/` | LangChain Agent 及 `ChatOrchestrator`、`MemoirOrchestrator` 等编排类 | +| 核心设施 | `app/core/` | 配置、数据库引擎、Redis、安全、日志、中间件、异常处理等 | +| 任务 | `api/tasks/` | Celery 应用与任务定义入口 | + + +### 4.2 功能域与源码对应关系 + +以下功能域均位于 `api/app/features/` 下,各自通常包含 `router`、`service`、`repo`(按需)、`schemas`、`deps` 等文件: + + +| 域 | 目录 | 主要职责 | +| --- | --------------- | ---------------------------------------------------- | +| 认证 | `auth/` | 注册、登录、短信验证码、JWT 签发与校验、头像预设等 | +| 用户 | `user/` | 用户资料、反馈等对外接口 | +| 对话 | `conversation/` | REST 会话管理、WebSocket 管线、`ChatOrchestrator` 调用、历史与额度守护 | +| 记忆 | `memory/` | 记忆块写入、向量检索、富化管道、压实服务、嵌入调度与对外查询接口 | +| 回忆录 | `memoir/` | 章节与阅读素材、PDF 服务、叙事安全、口述规范化、章节封面与正文图像任务、状态服务等 | +| 故事 | `story/` | 与篇章素材、同步写入、配图意图等相关的服务与仓储(与回忆录流水线协同) | +| 套餐 | `plan/` | 套餐目录与订购相关能力 | +| 配额 | `quota/` | 配额检查与扣减策略,供对话、回忆录、支付等场景注入使用 | +| 支付 | `payment/` | 订单、微信与支付宝等支付通道封装 | +| 任务 | `tasks/` | 任务状态查询等对前端的任务进度暴露 | +| 内容 | `content/` | TTS 等内容相关接口 | +| 资源 | `asset/` | 资源存取相关仓储与模型 | +| 评测 | `evaluation/` | 内部评测数据模型、打分、会话目录、回放、对比摘要等(由 `internal_main` 挂载) | + + +### 4.3 WebSocket 协议文件 + +工程在 `api/app/features/conversation/ws/protocol.md` 中维护 WebSocket 文本协议说明,包括连接 URL 形态、客户端到服务端与服务端到客户端的消息类型枚举、心跳与重连语义等,与实现代码共同构成可审查的契约说明。 + +--- + +## 第 5 章 功能规格说明 + +本章按业务域给出可测试的规格化描述:目标、典型输入、处理要点、输出与约束。 + +### 5.1 认证与用户(auth / user) + +**目标**:为全站提供可信身份,支撑受保护 REST 与 WebSocket。 + +**典型输入**:手机号与密码注册、登录;短信验证码发送与校验;刷新令牌请求。 + +**处理要点**:密码经安全哈希存储;签发短期访问令牌与长期刷新令牌;对外 schema 校验在 Pydantic 层完成。 + +**输出**:令牌对、用户基础资料、错误码与错误信息遵循 `app/core/errors.py` 约定的统一错误响应结构(含 `error_code`、`message`、`request_id`)。 + +**约束**:成功业务响应可为直接模型或字典,不必统一包装信封;敏感环境变量不得写入代码库。 + +### 5.2 对话与实时管线(conversation) + +**目标**:维持长连接会话,完成「语音或文本 →(可选 ASR)→ Agent 推理 →(可选 TTS)→ 客户端展示」闭环。 + +**典型输入**:`TEXT`、`AUDIO_SEGMENT`、`AUDIO_MESSAGE`、`TRANSCRIBE_ONLY`、`TTS_REQUEST`、`TTS_CANCEL`、`END_CONVERSATION`、心跳等,详见 `protocol.md`。 + +**处理要点**:连接 URL 为 `/ws/conversation/{conversation_id}?token={jwt_access_token}`;编排由 `ChatOrchestrator` 将用户消息路由到画像分支或访谈分支;会话真源写入 `conversation_messages`,Redis 用于加速与缓存;额度由 `quota` 相关守护在管线中发挥作用。 + +**输出**:`TRANSCRIPT`、`AGENT_RESPONSE` 分段、`TTS_AUDIO`、`MEMOIR_UPDATE`、`ERROR` 等下行消息。 + +**约束**:同一连接内消息顺序稳定;若本轮开启服务端 TTS,则每一助手分段先 `TTS_AUDIO` 再对应 `AGENT_RESPONSE`。 + +### 5.3 记忆与检索(memory) + +**目标**:将对话中可复用的事实与摘要以向量块形式持久化,支持后续会话与回忆录生成时的语义检索。 + +**典型输入**:会话轮次文本、用户标识、检索 query、富化开关相关配置。 + +**处理要点**:写入与切块、嵌入、可选 LLM 富化、定时压实(compaction)等由服务与 Celery 任务协同;异步与同步路径行为可能不同,工程文档中建议以 `docs/memory-retrieval.md`(若存在)等行为矩阵为准。 + +**输出**:检索到的记忆证据、供提示词注入的结构化片段、必要的追踪信息(若开启)。 + +**约束**:配置项如 `CHAT_MEMORY_RETRIEVAL_ENABLED`、`MEMORY_COMPACTION_ENABLED` 等由 `app/core/config.py` 集中管理,调参需结合运行环境与负载。 + +### 5.4 回忆录与 PDF(memoir) + +**目标**:将多轮叙事整理为结构化章节与阅读体验,并生成可导出的文档与配图。 + +**典型输入**:会话 id、回忆录 id、章节 id、导出请求、图像生成相关任务参数。 + +**处理要点**:`MemoirOrchestrator` 负责分段编排;Phase1 批次准备后 `process_memoir_phase1` 等 Celery 任务派发 Phase2,按类别进入 `run_story_pipeline_for_category_batch`;图像方面区分正文图 `generate_story_image` 与章节封面 `generate_chapter_cover` enqueue 逻辑;PDF 由 `pdf_service` 等模块调用排版引擎完成。 + +**输出**:章节文本、阅读片段、PDF 文件流或 URL、任务状态更新、经由 WebSocket 的 `MEMOIR_UPDATE` 通知。 + +**约束**:涉及对象存储 URI 时需经预签名或受控网关访问;长篇生成依赖队列与超时配置。 + +### 5.5 故事与素材(story) + +**目标**:承接回忆录流水线中篇章级写入与配图意图抽取等职责,与 `memoir` 域协同。 + +**典型输入**:批量单元规划结果、叙事路由 Agent 输出、图像意图字段。 + +**处理要点**:仓储与服务分层保持与全局架构一致;同步写入路径需考虑与数据库事务的一致性。 + +**输出**:持久化的故事单元记录、下游图像任务触发条件。 + +### 5.6 套餐与配额(plan / quota) + +**目标**:定义可售卖套餐与用户使用限额,避免超卖与滥用。 + +**典型输入**:套餐 id、用户当前订阅状态、对话轮次或回忆录导出等资源计量事件。 + +**处理要点**:`QuotaService` 供会话与回忆录等模块注入调用;不得在功能模块内硬编码配额 SQL。 + +**输出**:允许或拒绝决策、剩余额度字段(若接口暴露)。 + +### 5.7 支付与订单(payment) + +**目标**:完成下单、渠道拉起、回调处理与订单状态推进。 + +**典型输入**:支付方式枚举、订单金额、回调报文。 + +**处理要点**:微信与支付宝客户端由适配层封装;支付失败与幂等处理在服务层体现。 + +**输出**:支付参数包、订单状态、错误码。 + +### 5.8 任务进度(tasks) + +**目标**:向前端暴露长时间运行任务的状态,改善用户对回忆录生成等操作的感知。 + +**典型输入**:任务 id 或业务关联键。 + +**处理要点**:与 `task_tracker` 等核心组件协作(若启用)。 + +**输出**:状态枚举、进度百分比或步骤文案。 + +### 5.9 内容与朗读(content) + +**目标**:提供与朗读相关的服务端能力配合对话气泡交互。 + +**典型输入**:文本分段、音色参数(若支持)。 + +**处理要点**:与 TTS 适配层衔接;必要时写入缓存或以 URL 形式下发。 + +### 5.10 内部评测(evaluation) + +**目标**:支持离线或半离线环境下的会话回放、评分与对比报告生成,服务于研发质量保障。 + +**典型输入**:评测会话导入数据、评测脚本、评分 Rubric 版本。 + +**处理要点**:评测 HTTP API 运行在 `internal_app`;浏览器界面由 `app-eval-web` 调用上述 API。 + +**输出**:评测轨迹、对比摘要、门禁报告(gate report)等结构化结果。 + +--- + +## 第 6 章 核心业务流程与设计说明 + +### 6.1 逻辑分层与依赖关系 + +```mermaid +flowchart TB + subgraph clients [Clients] + ExpoApp[app-expo] + EvalWeb[app-eval-web] + end + subgraph backend [api FastAPI] + Router[features routers] + Service[services] + Repo[repos] + Agents[agents] + Ports[ports] + Adapters[adapters] + end + subgraph infra [Infrastructure] + PG[(PostgreSQL)] + Redis[(Redis)] + Celery[Celery workers] + end + ExpoApp --> Router + EvalWeb --> Router + Router --> Service + Service --> Repo + Service --> Agents + Service --> Ports + Ports --> Adapters + Repo --> PG + Service --> Redis + Service --> Celery +``` + + + +### 6.2 访问令牌认证与刷新(REST 概要) + +```mermaid +sequenceDiagram + participant C as Client + participant A as auth_router + participant S as AuthService + participant DB as PostgreSQL + C->>A: POST login credentials + A->>S: validate_and_issue_tokens + S->>DB: verify user password hash + DB-->>S: user row + S-->>A: access_token refresh_token + A-->>C: 200 JSON tokens + C->>A: GET protected with Authorization Bearer + A->>S: verify access_token + S-->>A: user context + A-->>C: 200 protected resource + C->>A: POST refresh with refresh_token + A->>S: rotate if valid + S-->>A: new token pair + A-->>C: 200 new tokens +``` + + + +### 6.3 WebSocket 对话主路径(概要) + +```mermaid +stateDiagram-v2 + [*] --> Connected: JWT valid connect + Connected --> Transcribing: AUDIO or AUDIO_SEGMENT + Transcribing --> AgentThink: TRANSCRIPT ready + Connected --> AgentThink: TEXT input + AgentThink --> StreamingReply: ChatOrchestrator yields + StreamingReply --> Connected: segments sent optional TTS + Connected --> Ending: END_CONVERSATION + Ending --> [*] + Connected --> Connected: PING PONG heartbeat +``` + + + +### 6.4 回忆录异步流水线(概要) + +```mermaid +flowchart LR + subgraph realtime [Realtime path] + WS[WebSocket pipeline] + CO[ChatOrchestrator] + DB1[(conversation_messages)] + end + subgraph batch [Batch path] + MO[MemoirOrchestrator] + P1[Phase1 Celery] + P2[Phase2 by category] + PDF[PDF service] + IMG[Image tasks] + end + WS --> CO + CO --> DB1 + DB1 --> MO + MO --> P1 + P1 --> P2 + P2 --> PDF + P2 --> IMG +``` + + + +### 6.5 记忆写入、富化与压实(概念) + +用户经对话产生的新素材经 `memory` 域服务切块与嵌入后进入 PostgreSQL/pgvector;可选富化任务进入独立 Celery 队列(如 `memory_idle`);压实任务按配置周期合并或清理旧块,以降低检索噪声与存储膨胀。详细行为以环境变量开关与代码内服务实现为准。 + +--- + +## 第 7 章 接口与数据概要 + +### 7.1 REST 与 WebSocket 入口形态 + +主应用 `app` 注册的路由前缀以各 `router` 定义为准;OpenAPI 文档在 `settings.enable_docs` 为真时可通过 `/docs` 与 `/redoc` 访问。WebSocket 固定路径模板为 `/ws/conversation/{conversation_id}`,鉴权参数为 Query 中的 `token`。 + +内部评测应用 `internal_app` 的评测相关接口前缀参见 `internal_main` 中的说明(例如 `/internal/api/evaluation/` 一类路径,以实际路由定义为准)。 + +### 7.2 统一错误响应 + +业务错误返回 JSON 结构包含字段:`error_code`、`message`、`request_id`,由全局异常处理器注册,保证客户端可程序化解析。 + +### 7.3 数据库与迁移 + +业务表模型分散于各 `features/*/models.py`,并在 `app/main.py` 中间接 import 以聚合到 `Base.metadata`。一切结构变更必须通过 `api/alembic` 版本脚本完成。应用启动时可在 `run_alembic_upgrade_at_startup` 中自动执行 `upgrade head`,生产可配置失败即退出。 + +### 7.4 核心实体(语义级) + + +| 实体 | 语义 | +| ---------------------- | ---------------------------- | +| User | 注册用户、凭据哈希、画像字段 | +| Conversation / Session | 对话容器,关联多条消息 | +| ConversationMessage | 单条用户或助手消息,WebSocket 与历史查询的真源 | +| Memoir / Chapter | 回忆录与章节树;阅读片段与导出依赖其内容 | +| MemoryChunk | 向量检索用的记忆块 | +| Order / Payment | 订单与支付流水 | +| Plan / Quota | 套餐定义与用户使用额度 | + + +--- + +## 第 8 章 开发状况与测试结果说明 + +### 8.1 当前实现状态(与仓库说明对齐) + +下列能力在根目录 `README.md` 中标记为已实现或持续演进,著作权文档在此做技术化转述: + +1. 用户注册与登录(手机号与密码)。 +2. 基于 JWT 的访问令牌与刷新令牌机制。 +3. 基于 WebSocket 的实时语音与文本对话。 +4. AI 引导访谈与画像采集分支(由 `ChatOrchestrator` 路由)。 +5. 语音识别与语音合成适配(具体厂商由配置与适配器决定)。 +6. 对话内容向章节与回忆录结构的整理与异步处理。 +7. 回忆录章节管理与 PDF 导出能力。 +8. Android 端本地数据能力(Room 等,详见客户端工程)。 +9. 离线数据同步相关能力(详见客户端工程)。 +10. 用户套餐与订单支付相关能力。 +11. 常见问题与反馈入口(用户域接口与客户端页面)。 + +标记为规划或进行中的能力(如更多传记模板、章节编辑深化、多语言等)以仓库 `README.md` 最新描述为准,本说明书不将其表述为已完成交付。 + +### 8.2 测试策略与自动化范围 + +后端测试以 `pytest` 为主,辅以 `httpx.AsyncClient` 与 `ASGITransport` 进行异步 HTTP 场景测试。项目规则强调: + +1. 测试应服务真实业务场景,避免仅为覆盖率堆砌无断言价值的用例。 +2. 优先覆盖核心用户路径、鉴权与配额、幂等性与关键持久化状态。 +3. 不优先测试第三方库自身行为或脆弱实现细节。 +4. 测试应用 `dependency_overrides` 替换数据库与外部依赖,避免直接 import 生产入口带来的环境副作用。 + +典型可自动化部分包括:注册登录与刷新链路、受保护资源访问、主要 CRUD 与失败分支。依赖真实 ASR、TTS、LLM、短信、支付沙箱的全链路测试多在手工或独立 E2E 环境执行。 + +### 8.3 测试结果表述方式建议 + +在向登记机关提交「测试结果」材料时,可将以下内容整理为附录表格或截图说明: + +1. 本地或 CI 执行 `uv run pytest` 的命令、通过用例数量与跳过用例原因。 +2. 关键模块的覆盖率报告(若政策允许附带 `pytest-cov` 摘要)。 +3. 手工测试用例清单:WebSocket 多轮对话、回忆录生成端到端、支付回调联调(若适用)。 + +本说明书不嵌入具体 CI 截图,以免与仓库演进不同步;导出 PDF 前由实施者替换为当期报告。 + +--- + +## 第 9 章 安装、配置与使用方法 + +### 9.1 获取源代码 + +通过版本控制系统克隆 `life-echo` 仓库,检查 out 的分支与标签与本次登记版本一致。 + +### 9.2 后端本地开发(推荐路径) + +在 `api/` 目录: + +1. 安装 `uv` 并执行 `uv sync --dev` 安装运行与开发依赖。 +2. 复制环境变量模板为 `.env`,按 `api/README.md` 填入数据库 URL、Redis URL、JWT 密钥、LLM 与 ASR 相关 Key 等。 +3. 使用 `docker compose -f docker-compose.dev.yml up -d` 或项目提供的一键脚本启动 PostgreSQL 与 Redis。 +4. 执行 `uv run alembic upgrade head`(若未依赖启动时自动迁移)。 +5. 执行 `uv run uvicorn main:app --reload --host 0.0.0.0 --port 8000` 或通过 `./dev-up.sh` 拉起 API 与 Celery。 + +环境变量优先级与 LLM 提供商选择逻辑以 `api/README.md` 与 `app/core/config.py` 为准。 + +### 9.3 内部评测入口 + +在 `api/` 目录执行 `uv run uvicorn app.internal_main:internal_app --host 0.0.0.0 --port 7999`(端口以实际环境变量为准)。在另一终端于 `app-eval-web/` 执行 `npm install` 与 `npm run dev`,通过浏览器访问 Vite 提示的本地地址操作评测台。 + +### 9.4 移动端使用 + +在 `app-expo/` 目录执行 `npm install` 与 `npx expo start`,按 Expo CLI 提示在模拟器或真机打开应用。将 API 基础地址指向已启动的后端(含合法 TLS 或开发环境豁免配置),完成登录后即可使用对话与回忆录功能。具体网络权限与麦克风权限以各平台 `app.json` / 插件配置为准。 + +### 9.5 生产部署注意 + +1. `SECRET_KEY` 与所有第三方密钥必须来自安全存储。 +2. CORS `allow_origins` 在生产应收敛为前端真实域名,而非通配。 +3. 建议将 `ALEMBIC_STARTUP_FAIL_FAST` 设为严格模式以避免实例在错误 Schema 下带病运行。 +4. Celery Worker 与 Beat 应与 API 分离进程或容器,并为 `memory_idle` 等专用队列配置合理并发。 + +### 9.6 日志与排错 + +业务代码使用 `app.core.logging.get_logger(__name__)` 取得 logger,便于按 request id 关联。出现 4xx/5xx 时优先检查错误体中的 `request_id` 与服务端日志同一时间窗口。 \ No newline at end of file