## Context yaml2pptx.py 当前是一个 1,245 行的单文件脚本,包含了从 YAML 解析到 PPTX 生成的完整流程。虽然功能完整,但代码组织存在以下问题: - **可读性差**:单文件过大,开发者和 LLM 工具难以快速理解代码结构 - **维护困难**:修改一个功能可能需要在文件中跳转多次,容易引入错误 - **扩展性受限**:添加新元素类型需要在多处添加代码(验证、PPTX 渲染、HTML 渲染) - **测试不便**:无法对单个模块进行独立测试 当前约束: - 必须保持 `uv run yaml2pptx.py` 的单入口使用方式 - 所有依赖通过 `/// script` 头部声明 - 不能污染主机环境的 Python 配置 ## Goals / Non-Goals **Goals:** - 将代码拆分为职责清晰的模块,每个文件控制在 150-300 行 - 引入元素抽象层,使添加新元素类型只需实现数据类和渲染方法 - 建立清晰的依赖关系,避免循环依赖 - 保持向后兼容,用户使用方式不变 - 为未来扩展(如添加 PDF 渲染器、Video 元素)奠定基础 **Non-Goals:** - 不改变现有功能的行为(纯重构,不添加新功能) - 不改变 YAML 文件格式或 CLI 参数 - 不迁移到标准 Python 包(保持单文件入口模式) - 不添加单元测试(可以在后续单独添加) ## Decisions ### 1. 四层架构设计 **决策**:采用 core/loaders/renderers/preview 四层架构 ``` html2pptx/ ├── yaml2pptx.py # 入口层:CLI + main ├── core/ # 核心层:领域模型 │ ├── elements.py │ ├── template.py │ └── presentation.py ├── loaders/ # 加载层:数据输入 │ └── yaml_loader.py ├── renderers/ # 渲染层:数据输出 │ ├── pptx_renderer.py │ └── html_renderer.py ├── preview/ # 预览层:可选功能 │ └── server.py └── utils.py # 工具层:通用函数 ``` **理由**: - **职责分离**:每层有明确的职责,修改影响范围可控 - **依赖方向清晰**:入口 → 渲染 → 核心 ← 加载,避免循环依赖 - **易于扩展**:添加新渲染器(如 PDF)只需在 renderers/ 下新增文件 **替代方案**: - 扁平结构(所有文件在同一目录):简单但缺乏层次,未来扩展会混乱 - 更细粒度的分层(如 domain/application/infrastructure):过度设计,不适合当前规模 ### 2. 元素抽象层使用 dataclass **决策**:使用 Python dataclass 定义元素数据类 ```python from dataclasses import dataclass, field @dataclass class TextElement: type: str = 'text' content: str = '' box: list[float] = field(default_factory=lambda: [1, 1, 8, 1]) font: dict = field(default_factory=dict) def __post_init__(self): # 创建时验证 if len(self.box) != 4: raise ValueError("box 必须包含 4 个数字") ``` **理由**: - **简洁性**:dataclass 自动生成 `__init__`、`__repr__` 等方法,减少样板代码 - **类型提示**:支持类型注解,IDE 和 LLM 工具可以提供更好的补全 - **验证时机**:`__post_init__` 在对象创建时自动调用,尽早发现错误 - **可扩展性**:未来可以添加方法(如 `to_dict()`、`validate_deep()`) **替代方案**: - TypedDict:运行时是字典,缺少验证能力 - 普通类:需要手写 `__init__`,代码冗长 ### 3. 渲染器内置在生成器中 **决策**:将渲染逻辑内置在 PptxGenerator 和 HtmlRenderer 类中 ```python class PptxGenerator: def add_slide(self, slide_data, base_path=None): # 渲染元素 for elem in slide_data['elements']: self._render_element(slide, elem, base_path) def _render_element(self, slide, elem, base_path): if isinstance(elem, TextElement): self._render_text(slide, elem) elif isinstance(elem, ImageElement): self._render_image(slide, elem, base_path) # ... ``` **理由**: - **封装性**:渲染逻辑与生成器紧密相关,内置更自然 - **简单性**:不需要额外的渲染器接口或依赖注入 - **性能**:避免额外的方法调用开销 **替代方案**: - 渲染器作为参数传入:增加复杂度,当前不需要运行时切换渲染器 - 统一渲染接口:过度抽象,PPTX 和 HTML 渲染差异较大 ### 4. 创建时验证 **决策**:在元素对象创建时进行验证(`__post_init__` 方法) **理由**: - **尽早失败**:在数据进入系统时就发现错误,而不是等到渲染时 - **清晰的错误位置**:验证失败时,堆栈指向元素创建处,易于定位 - **避免无效状态**:确保元素对象始终处于有效状态 **替代方案**: - 加载时验证:验证逻辑分散在 yaml_loader 中,难以维护 - 渲染时验证:错误发现太晚,可能已经生成了部分幻灯片 ### 5. 元素工厂函数 **决策**:提供 `create_element(elem_dict)` 工厂函数,从字典创建元素对象 ```python def create_element(elem_dict: dict): elem_type = elem_dict.get('type') if elem_type == 'text': return TextElement(**elem_dict) elif elem_type == 'image': return ImageElement(**elem_dict) # ... ``` **理由**: - **统一入口**:所有元素创建都通过工厂函数,便于添加通用逻辑(如日志、缓存) - **类型安全**:工厂函数可以进行类型检查,避免创建错误的元素类型 - **易于扩展**:添加新元素类型只需在工厂函数中添加一个分支 ### 6. 依赖管理策略 **决策**:所有依赖保留在 yaml2pptx.py 的 `/// script` 头部,预览依赖不再可选 ```python # /// script # requires-python = ">=3.8" # dependencies = [ # "python-pptx", # "pyyaml", # "flask", # "watchdog", # ] # /// ``` **理由**: - **简化处理**:不需要条件导入或可选依赖检查 - **一致性**:所有用户都有相同的依赖环境 - **依赖轻量**:flask 和 watchdog 都是轻量级库,不会显著增加安装时间 ## Risks / Trade-offs ### 风险 1:导入路径变化可能引入错误 **风险**:重构后内部导入路径会发生变化,可能遗漏某些导入或引入循环依赖 **缓解措施**: - 按模块逐步迁移,每迁移一个模块就测试一次 - 使用 Python 的 `import` 语句检查(运行脚本时会立即发现导入错误) - 保持清晰的依赖方向:入口 → 渲染 → 核心 ← 加载 ### 风险 2:元素验证可能过于严格 **风险**:在 `__post_init__` 中进行验证可能拒绝某些边缘情况的有效输入 **缓解措施**: - 只验证关键约束(如 box 必须有 4 个元素),不验证业务逻辑 - 提供清晰的错误消息,帮助用户快速定位问题 - 如果发现验证过严,可以在后续放宽 ### 风险 3:文件数量增加可能影响开发体验 **权衡**:从 1 个文件变成 8+ 个文件,开发者需要在多个文件间跳转 **缓解措施**: - 每个文件职责清晰,命名直观(如 `elements.py`、`pptx_renderer.py`) - 文件大小适中(150-300 行),易于在单个屏幕内浏览 - 提供清晰的模块文档,说明每个文件的职责 ### 风险 4:重构可能引入功能回归 **风险**:代码重组过程中可能无意中改变了某些行为 **缓解措施**: - 保持函数逻辑不变,只改变组织方式 - 重构后运行完整的功能测试(手动测试所有 YAML 示例) - 对比重构前后生成的 PPTX 文件,确保输出一致 ## Migration Plan ### 阶段 1:基础设施(utils + loaders) 1. 创建目录结构 2. 提取 `utils.py`(日志函数、颜色转换) 3. 提取 `loaders/yaml_loader.py`(YAML 加载和验证) 4. 更新 yaml2pptx.py 的导入,测试 YAML 加载功能 ### 阶段 2:核心抽象(core) 1. 实现 `core/elements.py`(元素数据类 + 工厂函数) 2. 提取 `core/template.py`(Template 类) 3. 提取 `core/presentation.py`(Presentation 类) 4. 测试模板渲染功能 ### 阶段 3:渲染器(renderers) 1. 实现 `renderers/pptx_renderer.py`(PptxGenerator + PPTX 渲染) 2. 实现 `renderers/html_renderer.py`(HtmlRenderer) 3. 测试 PPTX 生成和 HTML 预览功能 ### 阶段 4:预览功能(preview) 1. 提取 `preview/server.py`(Flask 服务器 + 文件监听) 2. 测试预览服务器功能 ### 阶段 5:入口整合 1. 重构 yaml2pptx.py(只保留 CLI + main) 2. 运行完整的端到端测试 3. 对比重构前后的输出,确保一致性 ### 回滚策略 - 保留原始 yaml2pptx.py 的备份(如 yaml2pptx.py.backup) - 如果发现严重问题,可以快速回滚到原始版本 - 使用 git 分支进行重构,确保可以随时回退 ## Open Questions 无待解决问题。设计方案已在 explore mode 中与用户充分讨论并确认。