## Context **当前状态**: - 项目已有完整的外部模板系统(core/template.py),包括变量替换、条件渲染等功能 - 模板系统采用文件隔离方式,模板定义在独立的 YAML 文件中 - 通过 `template: ` 引用外部模板,使用 `--template-dir` 指定模板目录 - 项目中没有任何实际使用模板的案例,说明外部模板的使用门槛较高 **问题分析**: 通过代码分析和 grep 搜索发现: 1. 整个项目中没有包含 `template:` 字段的 YAML 文件 2. 所有模板测试用例都是动态生成的,没有真实的用户使用场景 3. 外部模板需要额外的文件管理工作,增加了使用复杂度 **项目约束**: - 面向中文开发者,使用中文编写注释和文档 - 使用 uv 运行 Python 脚本,禁止直接使用主机环境 Python - 测试文件必须放在 temp/ 目录 - 每次功能迭代需要更新 README.md 和 README_DEV.md - 所有需求必须设计全面、合理的测试内容 ## Goals / Non-Goals **Goals:** - 支持在 YAML 源文件中定义和使用内联模板 - 保持与外部模板系统的兼容性,允许混合使用 - 禁止内联和外部模板同名,要求模板名称必须唯一 - 提供完整的错误处理和验证机制 - 保持代码向后兼容,不破坏现有功能 **Non-Goals:** - 不支持内联模板之间的相互引用 - 不支持内联模板引用外部模板 - 不实现模板继承或组合功能 - 不修改外部模板的现有功能 ## Decisions ### 决策 1: 使用 `templates` 字段定义内联模板 **选择**: 在 YAML 文件顶层新增 `templates` 字段,字典类型,键为模板名称,值为模板定义 **理由**: - 保持 YAML 结构清晰,与 `metadata`、`slides` 字段同级 - 字典结构便于查找和管理 - 不与现有字段冲突,向后兼容 **替代方案考虑**: - 方案 A: 在 `metadata` 中定义 templates - 会造成 metadata 职责混乱 - 方案 B: 使用 YAML 锚点语法 - 无法支持变量替换和条件渲染 - 方案 C: 在每个 slide 中定义内联模板 - 无法实现模板复用 ### 决策 2: 禁止内联和外部模板同名 **选择**: 当同名模板同时存在于内联和外部时,直接抛出 ERROR 级别错误,不允许使用该模板 **理由**: - 显式报错比隐式选择更安全,避免用户意外使用了错误的模板 - 内联和外部模板混合使用的情况较少,同名通常是错误或命名不当 - 强制用户明确模板来源,提升代码清晰度 - 降低调试难度,错误立即暴露而不是隐式选择 **实现**: ```python def get_template(self, template_name): # 1. 先检查内联模板 if hasattr(self, 'inline_templates') and template_name in self.inline_templates: # 2. 检查外部模板是否也存在同名 if self._external_template_exists(template_name): raise YAMLError( f"模板名称冲突: '{template_name}' 同时存在于内联模板和外部模板目录\n" f"请使用不同的模板名称以避免冲突" ) inline_data = self.inline_templates[template_name] return Template.from_data(inline_data, template_name) # 3. 回退到外部模板 if template_name not in self.template_cache: self.template_cache[template_name] = Template( template_name, self.templates_dir ) return self.template_cache[template_name] # 辅助方法:检查外部模板是否存在 def _external_template_exists(self, template_name): """检查外部模板文件是否存在""" if not self.templates_dir: return False template_path = Path(self.templates_dir) / f"{template_name}.yaml" return template_path.exists() ``` ### 决策 3: 新增 `Template.from_data()` 类方法 **选择**: 添加类方法从字典创建模板实例,而不是修改现有的 `__init__` **理由**: - 保持现有 `Template.__init__` 的不变性,专注于外部模板加载 - 区分内联和外部模板的创建路径,降低耦合度 - 类方法语义清晰:`Template.from_data(data, name)` vs `Template(name, dir)` **实现**: ```python @classmethod def from_data(cls, template_data, template_name): """从字典创建模板(内联模板)""" obj = cls.__new__(cls) obj.data = template_data obj.vars_def = {} for var in template_data.get('vars', []): obj.vars_def[var['name']] = var obj.elements = template_data.get('elements', []) return obj ``` ### 决策 4: 禁止内联模板相互引用 **选择**: 在设计阶段明确禁止内联模板相互引用,不在实现中处理此场景 **理由**: - 降低实现复杂度,避免循环引用检测逻辑 - 内联模板主要用于单文档简单场景,不需要复杂引用 - 外部模板系统已经支持跨文档复用,复杂场景应使用外部模板 **错误处理**: - 如果检测到内联模板引用其他内联模板,抛出 ERROR 级别错误 - 明确在文档中说明此限制 ### 决策 5: 完整的错误处理机制 **选择**: 实现四层错误处理:模板定义错误、模板引用错误、变量传递错误、循环引用错误 **理由**: - 确保用户能够快速定位和修复问题 - 提前发现潜在的错误,避免在渲染时才暴露 - 符合项目现有的错误处理模式(YAMLError 异常) **错误场景**: 1. 模板定义错误: `templates` 不是字典、缺少 `elements` 字段、变量定义错误 2. 模板引用错误: 引用的内联模板不存在、外部模板不存在 3. 变量传递错误: 缺少必需变量、传递未定义变量 4. 循环引用错误: 内联模板自引用(虽然禁止相互引用,但需要检测自引用) ## Risks / Trade-offs ### 风险 1: 内存占用增加 **风险**: 内联模板数据保存在内存中,可能增加内存占用 **缓解措施**: - 单个模板数据量通常很小(<1KB),内存增加可忽略 - 内联模板不需要缓存(已在内存中),不会重复占用 - 未来可以添加内存监控,如果问题严重再优化 ### 风险 2: YAML 文件可读性下降 **风险**: 大量的内联模板定义可能使 YAML 文件过长,降低可读性 **缓解措施**: - 内联模板适合简单场景,复杂场景建议使用外部模板 - 在文档中提供最佳实践建议 - IDE 的折叠功能可以帮助管理长文件 ### 风险 3: 模板命名冲突强制检测 **风险**: 内联和外部模板同名时,用户可能期望使用外部模板,但系统直接报错 **缓解措施**: - 明确的错误消息,指出模板冲突的具体位置(内联 templates 字段 vs 外部模板目录) - 在文档中详细说明命名规则,建议使用不同的命名空间或前缀 - 在示例代码中展示正确的命名方式 - 提供 migration 指南,帮助用户解决命名冲突 ### 风险 4: 循环引用误判 **风险**: 虽然禁止相互引用,但可能存在复杂的循环引用路径 **缓解措施**: - 实现简单的循环引用检测(DFS 遍历) - 在设计阶段就明确规则,降低实现复杂度 - 添加充分的测试用例覆盖边界情况 ## Open Questions 无。设计阶段已明确所有关键决策。