# PRD: App Factory + Config 统一重构(PR1) ## 背景 当前 `ephron.ren` 已经从单一 MVP 演进成多服务单仓结构,包含: - `auth` - `blog` - `canvas` - `home` - `prompt` - `shared` 但服务初始化与配置层仍保留大量早期复制实现,主要集中在: - 五个服务的 `src/main.py` - 五个服务的 `src/config.py` 这导致后续每次要统一安全头、health、docs、错误处理、配置约定时,都需要多处重复修改,并且很容易发生服务之间的行为漂移。 本 PRD 定义第一阶段重构:**只收敛 app 初始化骨架与 config 读取骨架,不改业务逻辑**。 --- ## 问题定义 ### 1. `main.py` 高度重复 五个服务都重复做了以下工作: - 调用 `validate_config()` - 创建 `FastAPI(...)` - 安装 `security headers` - 创建并挂载 `limiter` - 挂载 `static` - 注册 routers - 注册 404/500 handler - 提供 `/health` 这种重复意味着: - 加一个通用能力要改 5 份 - 某个服务漏改的概率很高 - 测试也更难形成统一契约 ### 2. `config.py` 既重复又不一致 当前配置层存在两个问题: #### 重复 各服务都各自实现了: - `.env` 加载 - 必填/可选环境变量读取 - config summary 输出 - 环境模式判断 #### 不一致 - `home` 使用 `ENVIRONMENT` - 其他服务使用 `ENV` 这会提高部署、调试和文档维护成本。 ### 3. 当前 shared 层只抽了一半 项目已经有这些共享能力: - `shared/security_headers.py` - `shared/limiter.py` - `shared/health.py` - `shared/templating.py` - `shared/ports.py` 说明项目已经具备共享层思路,但还没有把最核心的 app/config 装配流程统一起来。 --- ## 本次目标 本 PR1 完成后,应该达到: 1. 五个服务 `main.py` 只保留: - service 专属 lifespan - router 列表 - service 元信息 - 调用统一 app factory 2. 五个服务 `config.py` 改为复用共享 helper: - `.env` 加载 - required/optional env 读取 - 环境模式判定 - summary 输出 3. `ENV` 成为统一主环境变量,`home` 暂时兼容 `ENVIRONMENT` 4. 不改变外部行为: - URL 不变 - `/health` 响应结构不变 - 404/500 页面行为兼容 - dev 模式 docs_url 保持可用 --- ## 非目标 这次**不做**: - 不重做 service API - 不收敛数据库 migration 归属 - 不彻底消灭所有 `sys.path.insert` - 不迁移目录结构 - 不改业务权限与业务逻辑 这些属于后续 PR。 --- ## 方案设计 ## 一、共享 app factory 新增: - `shared/app_factory.py` 职责: - 创建标准化 `FastAPI` app - 安装安全头 - 安装 limiter - 挂载静态目录 - 注册 routers - 注册默认错误处理 - 注册 `/health` 建议接口: ```python def create_service_app( *, service_name: str, title: str, description: str, version: str, is_development: bool, templates_dir, routers: list, static_dir=None, lifespan=None, ): ... ``` ### 兼容要求 - `service_name` 用于 health 响应,例如 `blog.ephron.ren` - `static_dir=None` 时跳过 static 挂载 - `canvas` 当前是“目录存在才挂载”,新实现必须兼容 --- ## 二、共享错误处理安装器 新增: - `shared/error_handlers.py` 职责: - 安装 404/500 handler - `/api/...` 返回 JSON - 页面路径返回模板 - production 不暴露原始异常文本 - development 可注入错误信息方便调试 建议接口: ```python def install_default_error_handlers(app, *, templates_dir, is_development): ... ``` ### 兼容要求 - API 404 继续返回 `{"detail": "Not Found"}` - 页面 404/500 继续使用各服务自己模板目录中的 `404.html` / `500.html` --- ## 三、共享 config helper 新增: - `shared/config_base.py` 职责: - 加载服务目录下 `.env` - 提供 required/optional env 读取 helper - 统一环境模式判定 - 统一 config summary 输出 建议接口: ```python from pathlib import Path def load_service_env(service_root: Path) -> None: ... def get_required_env(key: str) -> str: ... def get_optional_env(key: str, default: str) -> str: ... def resolve_environment(*, legacy_key: str | None = None) -> tuple[str, bool]: ... def print_config_summary(service_name: str, items: dict[str, str]) -> None: ... ``` ### 兼容要求 - `resolve_environment(legacy_key="ENVIRONMENT")` 在 `ENV` 缺失时回退旧字段 - 若 `ENV` 和 `ENVIRONMENT` 同时存在,以 `ENV` 为准 --- ## 具体实施步骤 ## Step 1:先补 shared 层单元测试 新增: - `tests/test_shared_config_base.py` - `tests/test_error_handlers.py` - `tests/test_app_factory.py` 覆盖这些能力: ### `test_shared_config_base.py` - 能加载指定服务目录 `.env` - required env 缺失时报清晰错误 - `ENV` 优先于 `ENVIRONMENT` - summary 输出格式稳定 ### `test_error_handlers.py` - `/api/...` 404 返回 JSON - 页面 404 返回模板 - production 500 不暴露原始异常文本 - development 500 可显示 `error_message` ### `test_app_factory.py` - 自动安装安全头 - 自动安装 limiter - 自动挂 routers - `static_dir` 存在时挂载 `/static` - `static_dir=None` 时不挂载 - 自动提供 `/health` - dev 模式 docs_url 存在,prod 模式关闭 --- ## Step 2:实现 `shared/config_base.py` 约束: - 共享层只处理“如何读配置”,不处理每个服务自己的业务字段 - 不在这里写数据库或业务逻辑 --- ## Step 3:实现 `shared/error_handlers.py` 要求: - 内部复用 `shared.templating.create_templates` - 用 path 前缀判断 API/页面行为 - production 不直接暴露 `str(exc)` --- ## Step 4:实现 `shared/app_factory.py` 应复用: - `shared.security_headers.install_security_headers` - `shared.limiter.create_limiter` - `shared.error_handlers.install_default_error_handlers` - `shared.health.build_health_response` 不要做的事: - 不在 factory 中写数据库初始化 - 不在 factory 中塞业务判断 factory 只负责通用 app 壳。 --- ## Step 5:改造五个服务 `config.py` 涉及: - `auth/src/config.py` - `blog/src/config.py` - `canvas/src/config.py` - `home/src/config.py` - `prompt/src/config.py` ### 逐服务注意点 #### Auth 保留: - `AUTH_SECRET_KEY` - `DATABASE_PATH` - `COOKIE_DOMAIN` - `COOKIE_NAME` - `TOKEN_MAX_AGE` - `TEMPLATES_DIR` 要求: - `validate_config()` 不变 - summary 输出语义基本不变 #### Blog 保留: - `CONTENT_DIR` 校验 - `CACHE_DIR.mkdir(...)` - `SEARCH_INDEX_DIR.mkdir(...)` #### Canvas 保留: - `CONTENT_DIR` 不存在时自动创建 不能把当前“自动建目录”改成“配置错误退出”。 #### Prompt 保留: - `DATABASE_PATH` - `TOKEN_MAX_AGE` - 当前 summary 语义 #### Home 重点: - 进入统一 config helper 体系 - 兼容 `ENVIRONMENT` - `AUTH_BASE_URL / AUTH_LOGIN_URL` 行为不变 必须验证: - 仅设置 `ENVIRONMENT=development` 时仍能进入开发模式 - 若同时设置 `ENV=production` 和 `ENVIRONMENT=development`,以 `ENV` 为准 --- ## Step 6:改造五个服务 `main.py` 涉及: - `auth/src/main.py` - `blog/src/main.py` - `canvas/src/main.py` - `home/src/main.py` - `prompt/src/main.py` 目标: - 删除重复 app 初始化逻辑 - 改为调用 `shared.app_factory.create_service_app(...)` 每个服务保留: - service 专属 lifespan - router 列表 - service 元信息 ### 特别注意:Home 的 `/health` `home/src/routes/pages.py` 当前也定义了 `/health`。 这次应统一只保留一处,建议: - 删除 `pages.py` 中的 `/health` - 统一由 app factory 提供 否则容易出现重复注册或行为漂移。 --- ## Step 7:调整回归测试 建议检查并更新: - `tests/test_security_hardening.py` - `tests/test_frontend_backend_reuse_contract.py` 新增建议断言: - 所有服务继续使用 shared template factory - 所有服务通过统一 app factory 提供 health - 所有服务继续保留安全头 - home 环境兼容逻辑同时覆盖 `ENV` 与 `ENVIRONMENT` --- ## Step 8:完整验证 建议执行: ```bash python -m pytest tests -q python -m pytest auth/tests -q python -m pytest blog/tests -q python -m pytest canvas/tests -q python -m pytest home/tests -q python -m pytest prompt/tests -q ``` 如本地可启动,再手动验证: ```bash python main.py --reload ``` 检查: - 五个服务均能启动 - `/health` 正常 - API 不存在路径返回 JSON 404 - 页面不存在路径返回各自 404 模板 --- ## 预计改动文件 ### 新增 - `shared/config_base.py` - `shared/error_handlers.py` - `shared/app_factory.py` - `tests/test_shared_config_base.py` - `tests/test_error_handlers.py` - `tests/test_app_factory.py` ### 修改 - `auth/src/config.py` - `blog/src/config.py` - `canvas/src/config.py` - `home/src/config.py` - `prompt/src/config.py` - `auth/src/main.py` - `blog/src/main.py` - `canvas/src/main.py` - `home/src/main.py` - `prompt/src/main.py` - `home/src/routes/pages.py`(删除重复 `/health`) - `tests/test_security_hardening.py` - `tests/test_frontend_backend_reuse_contract.py` --- ## 风险与注意事项 ### 1. Home 环境变量兼容风险 历史部署如果只写了 `ENVIRONMENT`,不能因为统一而直接失效。 ### 2. 404/500 行为变化风险 抽象错误处理时,很容易把 API 路径判断或模板上下文改坏,必须用单元测试兜住。 ### 3. Canvas static 行为变化风险 当前是“存在才挂载”,不能在 app factory 中粗暴统一成“总是挂载”。 ### 4. 测试依赖旧实现细节 如果某些测试直接断言 `main.py` 中出现某段文本,改造后会失效,需要调整为验证行为而不是验证源码写法。 --- ## 建议提交拆分 建议拆成 5 个 commit,便于实现与 review: 1. `test: add shared config and app factory coverage` 2. `feat: add shared config base` 3. `feat: add shared error handlers and app factory` 4. `refactor: migrate service config modules to shared helpers` 5. `refactor: migrate service main modules to shared app factory` --- ## 完成定义 本 PR1 完成后,必须满足: - [ ] 五个服务 `main.py` 不再手写重复 app 装配 - [ ] 五个服务 `config.py` 使用共享 helper - [ ] `ENV` 成为统一主环境变量,home 兼容 `ENVIRONMENT` - [ ] `/health` 外部行为不变 - [ ] 404/500 行为兼容 - [ ] 全部现有测试通过,新增 shared 测试通过 - [ ] 没有新增 `sys.path.insert` 使用点 --- ## 后续建议 PR1 合并后,建议继续: 1. PR2:抽 `shared/service_api/*` 与 `tests/helpers/*` 2. PR3:逐步收敛 import 结构,减少 `src` 冲突与 `sys.path.insert` 3. PR4:统一 DB schema / migration ownership 这样可以保证每一步都是建立在更稳定的基础上,而不是继续在重复结构上迭代。