Files
ephron-ren-prd/prd-app-factory-config-unification.md

471 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
这样可以保证每一步都是建立在更稳定的基础上,而不是继续在重复结构上迭代。