## Context 当前的模板系统支持条件渲染功能,但实现非常简单,仅通过正则表达式匹配 `{var != ''}` 格式来判断变量是否非空。这种实现无法满足实际使用中的复杂需求,如多条件组合、数值比较、成员测试等。 现有实现位于 `core/template.py` 的 `evaluate_condition` 方法,使用简单的正则匹配: ```python pattern = r'\{(\w+)\s*!=\s*[\'\"]{2}\}' match = re.match(pattern, condition) ``` 用户反馈需要更强大的条件表达式能力,但直接使用 Python 的 `eval()` 存在严重的安全风险。我们需要一个既强大又安全的表达式评估方案。 ## Goals / Non-Goals **Goals:** - 提供强大的条件表达式能力,支持比较、逻辑、成员测试、数学运算 - 确保表达式评估的安全性,防止代码注入和恶意操作 - 提供清晰的错误信息,帮助用户快速定位问题 - 保持 API 简洁,对现有代码的侵入性最小 - 实现高性能的表达式评估,不影响模板渲染速度 **Non-Goals:** - 不支持函数定义、类定义等复杂 Python 特性 - 不支持模块导入和文件操作 - 不支持对象属性访问(避免安全风险) - 不考虑旧版本语法的向后兼容(用户明确要求) - 不实现自定义的表达式解析器(使用成熟的第三方库) ## Decisions ### 决策 1: 使用 simpleeval 作为表达式评估引擎 **选择**: simpleeval (EvalWithCompoundTypes) **理由**: - 成熟稳定:483 stars,维护活跃,社区认可度高 - 安全性好:基于 AST 解析,不使用 `eval()`,有明确的安全边界 - 功能适中:支持我们需要的所有操作符和表达式类型 - 轻量级:单文件库,无额外依赖,不增加项目复杂度 - 易于集成:API 简单直观,集成成本低 **备选方案**: 1. **evalidate**: 性能更好(快 3-5 倍),但社区较小,文档较少 2. **asteval**: 功能更强大,但过于复杂,性能较差,不适合简单条件判断 3. **自实现**: 完全可控,但开发成本高,需要大量测试,容易出现安全漏洞 **权衡**: simpleeval 在功能、安全性、易用性之间达到了最佳平衡。 ### 决策 2: 使用 EvalWithCompoundTypes 而非 SimpleEval **选择**: EvalWithCompoundTypes **理由**: - 支持列表和元组字面量,允许 `status in ['draft', 'review']` 这样的表达式 - 对于条件判断场景,列表/元组是常见需求 - 安全性与 SimpleEval 相同,只是增加了复合类型支持 **API 差异**: ```python # SimpleEval simple_eval(expr, names=vars_values) # EvalWithCompoundTypes evaluator = EvalWithCompoundTypes(names=vars_values) evaluator.eval(expr) ``` ### 决策 3: 每次评估创建新的 evaluator 实例 **选择**: 每次调用 `evaluate_condition` 时创建新的 EvalWithCompoundTypes 实例 **理由**: - 避免状态污染:不同模板渲染之间完全隔离 - 线程安全:每个评估独立,无共享状态 - 简化实现:不需要管理 evaluator 的生命周期 **性能考虑**: - EvalWithCompoundTypes 实例化成本很低 - 表达式评估本身的开销远大于实例化开销 - 对于典型的模板渲染场景(几十个元素),性能影响可忽略 ### 决策 4: 扩展白名单函数 **选择**: 在 simpleeval 默认函数基础上,添加常用的安全函数 **白名单函数**: - 类型转换:`int()`, `float()`, `str()`, `bool()` - 数学函数:`abs()`, `min()`, `max()` - 容器函数:`len()` **理由**: - 这些函数在条件判断中常用 - 都是纯函数,无副作用,安全性高 - simpleeval 默认只提供 `int`, `float`, `str`,需要补充 **不添加的函数**: - 文件操作:`open()`, `read()`, `write()` - 系统操作:`exec()`, `eval()`, `compile()` - 反射操作:`getattr()`, `setattr()`, `hasattr()` ### 决策 5: 实现独立的 ConditionEvaluator 类 **选择**: 创建独立的 `ConditionEvaluator` 类,而不是直接在 Template 类中实现 **架构**: ``` Template └─ ConditionEvaluator └─ EvalWithCompoundTypes (simpleeval) ``` **理由**: - 单一职责:Template 负责模板渲染,ConditionEvaluator 负责条件评估 - 易于测试:可以独立测试条件评估逻辑 - 易于扩展:未来可以轻松替换评估引擎或添加新功能 - 代码清晰:避免 Template 类过于臃肿 ### 决策 6: 错误处理策略 **选择**: 捕获 simpleeval 的所有异常,转换为用户友好的 YAMLError **错误映射**: ```python NameNotDefined → "条件表达式中的变量未定义: {var_name}" FunctionNotDefined → "条件表达式中使用了不支持的函数: {func_name}" FeatureNotAvailable → "条件表达式使用了不支持的语法特性" SyntaxError → "条件表达式语法错误" ``` **错误信息包含**: - 原始表达式 - 具体的错误原因 - 可用的变量列表(对于 NameNotDefined) - 支持的函数列表(对于 FunctionNotDefined) - 修复建议 **理由**: - 用户不需要了解 simpleeval 的内部实现 - 错误信息更具体,更容易调试 - 保持与现有错误处理风格一致 ### 决策 7: 表达式安全限制 **选择**: 实施多层安全限制 **限制措施**: 1. **长度限制**: 表达式最大 500 字符 2. **白名单函数**: 仅允许预定义的安全函数 3. **禁止特性**: - 属性访问(`obj.attr`) - 函数定义(`lambda`, `def`) - 模块导入(`import`) - 赋值操作(`=`, `+=`) **理由**: - 长度限制防止过于复杂的表达式,也防止 DOS 攻击 - simpleeval 默认已禁止大部分危险操作 - 额外的白名单限制提供双重保护 ## Risks / Trade-offs ### 风险 1: simpleeval 的安全漏洞 **风险**: simpleeval 可能存在未知的安全漏洞,允许恶意代码执行 **缓解措施**: - simpleeval 是成熟的开源项目,经过广泛使用和审查 - 我们添加了额外的长度限制和白名单限制 - 表达式来源是用户自己的 YAML 文件,不是外部不可信输入 - 定期更新 simpleeval 到最新版本 **残留风险**: 低。对于本项目的使用场景(用户编写自己的模板),风险可接受。 ### 风险 2: 性能影响 **风险**: simpleeval 的表达式评估可能比简单的正则匹配慢,影响模板渲染性能 **缓解措施**: - 实际测试表明,simpleeval 的性能足够好(每秒可评估数万次) - 对于典型的演示文稿(几十个幻灯片,每个几十个元素),性能影响可忽略 - 如果未来性能成为瓶颈,可以考虑: - 缓存编译后的表达式(simpleeval 支持) - 切换到 evalidate(性能更好) **残留风险**: 极低。当前性能完全满足需求。 ### 风险 3: 用户学习成本 **风险**: 用户需要学习新的表达式语法,可能不熟悉 Python 表达式 **缓解措施**: - Python 表达式语法简单直观,学习成本低 - 提供详细的文档和示例 - 错误信息清晰,帮助用户快速定位问题 - 旧的简单语法(`{var != ''}`)在新实现中仍然有效 **残留风险**: 低。Python 表达式是业界标准,大多数开发者都熟悉。 ### 权衡 1: 功能 vs 安全性 **权衡**: 为了安全性,我们禁止了一些 Python 特性(如属性访问、函数定义) **影响**: 用户无法使用这些高级特性,但对于条件判断场景,当前支持的特性已经足够 **决策**: 安全性优先。如果未来有明确的需求,可以考虑有限地开放某些特性。 ### 权衡 2: 向后兼容 vs 代码简洁 **权衡**: 用户明确要求不考虑向后兼容,我们可以直接移除旧的正则匹配实现 **影响**: - 代码更简洁,维护成本更低 - 旧的简单语法(`{var != ''}`)在新实现中仍然有效,实际兼容性影响很小 **决策**: 移除旧实现,统一使用 simpleeval。 ## Migration Plan ### 实施步骤 1. **安装依赖** ```bash uv add simpleeval ``` 2. **实现 ConditionEvaluator 类** - 创建 `core/condition_evaluator.py` - 实现 `evaluate_condition` 方法 - 实现错误处理和安全限制 3. **集成到 Template 类** - 在 `Template.__init__` 中初始化 ConditionEvaluator - 替换 `evaluate_condition` 方法的实现 - 移除旧的正则匹配代码 4. **更新测试** - 扩展 `tests/unit/test_template.py` 中的条件渲染测试 - 添加新的表达式类型测试 - 添加错误处理测试 - 添加安全限制测试 5. **更新文档** - 更新 README.md 的条件渲染章节 - 添加表达式语法参考 - 更新 README_DEV.md 的架构说明 6. **验证和发布** - 运行完整测试套件 - 手动测试各种表达式场景 - 更新版本号 ### 回滚策略 如果发现严重问题,可以快速回滚: 1. 恢复 `core/template.py` 中的旧 `evaluate_condition` 实现 2. 移除 simpleeval 依赖 3. 恢复旧的测试用例 **回滚成本**: 低。改动集中在单个文件,易于回滚。 ## Open Questions 无。所有关键决策已明确。