feat(rules): 条件系统增强,支持 all/any 组合条件和 original/current 数据源切换

- evaluate_condition 支持递归 all/any 组合节点和 source 字段
- header_rules 支持 condition 条件触发,HeaderBuilder.apply_rules 透传 body/original_body
- 提取 EndpointConditionEditor 组件统一请求头/请求体规则的条件编辑 UI
- header_rules 新增服务端结构校验(action/key/from/to/condition)
- 新增组合条件、source 切换、fail-closed 等测试用例
This commit is contained in:
fawney19
2026-03-15 20:27:53 +08:00
parent 6b23c9b3ce
commit 3c7ad81d62
14 changed files with 1395 additions and 314 deletions

View File

@@ -1,5 +1,6 @@
import json
from src.api.handlers.base.request_builder import evaluate_condition
from src.core.api_format import (
CORE_REDACT_HEADERS,
HeaderBuilder,
@@ -93,6 +94,68 @@ class TestHeaderBuilder:
parsed = json.loads(normalized)
assert "d:\\桌面\\123\\Aether" in parsed["workspaces"]
def test_apply_rules_supports_nested_conditions(self) -> None:
builder = HeaderBuilder()
builder.apply_rules(
[
{
"action": "set",
"key": "X-Flag",
"value": "1",
"condition": {
"all": [
{"path": "metadata.mode", "op": "eq", "value": "prod"},
{
"any": [
{"path": "tier", "op": "eq", "value": "gold"},
{"path": "tier", "op": "eq", "value": "silver"},
]
},
]
},
}
],
body={"metadata": {"mode": "prod"}, "tier": "silver"},
condition_evaluator=evaluate_condition,
)
assert builder.build()["X-Flag"] == "1"
def test_apply_rules_supports_original_source(self) -> None:
builder = HeaderBuilder()
builder.apply_rules(
[
{
"action": "set",
"key": "X-Original",
"value": "yes",
"condition": {
"path": "metadata.mode",
"op": "eq",
"value": "prod",
"source": "original",
},
}
],
body={"metadata": {"mode": "test"}},
original_body={"metadata": {"mode": "prod"}},
condition_evaluator=evaluate_condition,
)
assert builder.build()["X-Original"] == "yes"
def test_apply_rules_fail_closed_without_body_or_evaluator(self) -> None:
builder = HeaderBuilder()
builder.apply_rules(
[
{
"action": "set",
"key": "X-Skip",
"value": "1",
"condition": {"path": "flag", "op": "eq", "value": True},
}
]
)
assert "X-Skip" not in builder.build()
class TestBuildUpstreamHeaders:
def test_priority_and_drop_headers(self) -> None:
@@ -143,6 +206,24 @@ class TestBuildUpstreamHeaders:
result = build_upstream_headers_for_endpoint({}, "openai:chat", "provider")
assert result["Content-Type"] == "application/json"
def test_header_rules_can_use_condition_against_body(self) -> None:
result = build_upstream_headers_for_endpoint(
{},
"openai:chat",
"provider",
header_rules=[
{
"action": "set",
"key": "X-Conditional",
"value": "1",
"condition": {"path": "mode", "op": "eq", "value": "prod"},
}
],
body={"mode": "prod"},
condition_evaluator=evaluate_condition,
)
assert result["X-Conditional"] == "1"
class TestFilterResponseHeaders:
def test_drops_hop_by_hop_and_body_dependent_headers(self) -> None: