1
0
Files
PPTX/openspec/changes/archive/2026-03-03-enhance-visible-condition-rendering/design.md
lanyuanxiaoyao 16ca9d77cd feat: 增强模板条件渲染表达式支持
使用 simpleeval 库替换原有的简单正则匹配,支持复杂的条件表达式评估。新增 ConditionEvaluator 类处理条件逻辑,支持比较运算、逻辑运算、成员测试、数学计算和内置函数,同时保持向后兼容性。
2026-03-03 17:28:23 +08:00

8.9 KiB
Raw Blame History

Context

当前的模板系统支持条件渲染功能,但实现非常简单,仅通过正则表达式匹配 {var != ''} 格式来判断变量是否非空。这种实现无法满足实际使用中的复杂需求,如多条件组合、数值比较、成员测试等。

现有实现位于 core/template.pyevaluate_condition 方法,使用简单的正则匹配:

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 差异:

# 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

错误映射:

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. 安装依赖

    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

无。所有关键决策已明确。