feat: body_rules 支持 condition 条件触发

为每条 body_rule 新增可选 condition 字段,支持 eq/neq/gt/lt/gte/lte/
starts_with/ends_with/contains/matches/exists/not_exists/in/type_is
共 14 种操作符,规则仅在条件满足时执行。

后端: 新增 _evaluate_condition 条件评估器与 _validate_condition 校验逻辑
前端: EndpointFormDialog 增加条件编辑行(IF 面板)与 Filter 按钮切换
测试: 新增 TestConditionalBodyRules 覆盖全部操作符及链式触发场景
This commit is contained in:
fawney19
2026-02-11 00:50:08 +08:00
parent 262bc6e1f7
commit 3d6d4a48a5
5 changed files with 1294 additions and 166 deletions

View File

@@ -34,7 +34,7 @@ from src.core.api_format import (
from src.core.crypto import crypto_service
from src.core.logger import logger
from src.core.provider_oauth_utils import enrich_auth_config, post_oauth_token
from src.models.endpoint_models import parse_re_flags
from src.models.endpoint_models import _CONDITION_OPS, _TYPE_IS_VALUES, parse_re_flags
if TYPE_CHECKING:
from src.models.database import ProviderAPIKey, ProviderEndpoint
@@ -539,6 +539,117 @@ def _resolve_original_placeholder(template: Any, original: Any) -> Any:
return template
# ==============================================================================
# 条件评估器
# ==============================================================================
# _CONDITION_OPS / _TYPE_IS_VALUES 从 endpoint_models 导入,避免重复定义
_SIMPLE_TYPE_MAP: dict[str, type] = {
"string": str,
"array": list,
"object": dict,
}
def _evaluate_condition(body: dict[str, Any], condition: dict[str, Any]) -> bool:
"""
评估单个条件表达式,决定规则是否应该执行。
条件格式: {"path": "model", "op": "starts_with", "value": "claude"}
条件无效时返回 False跳过该规则fail-closed
"""
if not isinstance(condition, dict):
return False
op = condition.get("op")
if not isinstance(op, str) or op not in _CONDITION_OPS:
return False
path = condition.get("path")
if not isinstance(path, str) or not path.strip():
return False
found, current_val = _get_nested_value(body, path.strip())
# 存在性检查:不需要 value
if op == "exists":
return found
if op == "not_exists":
return not found
# 其他操作符要求字段存在
if not found:
return False
expected = condition.get("value")
# 相等/不等
if op == "eq":
return current_val == expected
if op == "neq":
return current_val != expected
# 数值比较
if op in ("gt", "lt", "gte", "lte"):
if not isinstance(current_val, (int, float)) or not isinstance(expected, (int, float)):
return False
if op == "gt":
return current_val > expected
if op == "lt":
return current_val < expected
if op == "gte":
return current_val >= expected
return current_val <= expected # lte
# 字符串操作
if op == "starts_with":
return (
isinstance(current_val, str)
and isinstance(expected, str)
and current_val.startswith(expected)
)
if op == "ends_with":
return (
isinstance(current_val, str)
and isinstance(expected, str)
and current_val.endswith(expected)
)
if op == "contains":
if isinstance(current_val, str) and isinstance(expected, str):
return expected in current_val
if isinstance(current_val, list):
return expected in current_val
return False
if op == "matches":
if not isinstance(current_val, str) or not isinstance(expected, str):
return False
try:
return re.search(expected, current_val) is not None
except re.error:
return False
# 列表包含
if op == "in":
return isinstance(expected, list) and current_val in expected
# 类型判断
if op == "type_is":
if not isinstance(expected, str) or expected not in _TYPE_IS_VALUES:
return False
# bool 是 int 的子类,需要特殊处理
if expected == "number":
return isinstance(current_val, (int, float)) and not isinstance(current_val, bool)
if expected == "boolean":
return isinstance(current_val, bool)
if expected == "null":
return current_val is None
return isinstance(current_val, _SIMPLE_TYPE_MAP[expected])
return False
def apply_body_rules(
body: dict[str, Any],
rules: list[dict[str, Any]],
@@ -585,6 +696,11 @@ def apply_body_rules(
if not isinstance(rule, dict):
continue
# 条件触发condition 存在且不满足时跳过规则
condition = rule.get("condition")
if condition is not None and not _evaluate_condition(result, condition):
continue
action = rule.get("action")
if not isinstance(action, str):
continue