feat(scenarios): 兼容非法配置并展示错误摘要

This commit is contained in:
2026-05-30 00:36:31 +08:00
parent c57ab2f194
commit f7e0d8e4d8
4 changed files with 152 additions and 10 deletions

View File

@@ -36,15 +36,15 @@ def _get_nested(config: dict, path: tuple[str, ...]):
def validate_scenario(config: dict) -> dict:
# 校验真正影响运行闭环的必填字段;
# 页面展示字段如 applicable_questions 允许缺失,并在归一化阶段补默认值。
# 校验真正影响运行闭环的必填字段;
# 页面展示字段允许缺失,并在归一化阶段补默认值。
for field_path in REQUIRED_FIELDS:
_get_nested(config, field_path)
return normalize_scenario(config)
def normalize_scenario(config: dict) -> dict:
"""补齐页面和其模块常用的派生字段,减少模板中的条件判断。"""
"""补齐页面和其模块常用的派生字段,避免模板层重复判断。"""
normalized = dict(config)
normalized["applicable_questions"] = list(config.get("applicable_questions") or [])
normalized["rag"] = dict(config.get("rag", {}))
@@ -62,16 +62,47 @@ def _scenario_files() -> list[Path]:
return sorted([*config_dir.glob("*.yaml"), *config_dir.glob("*.yml")])
def _read_yaml_file(path: Path) -> dict:
with path.open("r", encoding="utf-8") as file:
return yaml.safe_load(file) or {}
def _collect_scenario_load_result() -> tuple[list[dict], list[dict]]:
"""
统一读取配置目录中的所有场景文件。
返回值:
- scenarios: 校验通过的场景列表
- issues: 非法 YAML / 缺字段等错误摘要,供首页展示
"""
scenarios = []
issues = []
for path in _scenario_files():
try:
config = _read_yaml_file(path)
scenarios.append(validate_scenario(config))
except (yaml.YAMLError, ScenarioValidationError) as exc:
issues.append(
{
"file_name": path.name,
"message": str(exc),
}
)
return scenarios, issues
def list_scenarios() -> list[dict]:
# 首页每次读取最新 YAML便于复试现场快速改题。
scenarios = []
for path in _scenario_files():
with path.open("r", encoding="utf-8") as file:
config = yaml.safe_load(file) or {}
scenarios.append(validate_scenario(config))
scenarios, _issues = _collect_scenario_load_result()
return scenarios
def list_scenario_issues() -> list[dict]:
"""返回配置异常摘要,便于页面明确提示而不是直接 500。"""
_scenarios, issues = _collect_scenario_load_result()
return issues
def get_scenario(scenario_id: str) -> dict:
for scenario in list_scenarios():
if scenario["id"] == scenario_id:

View File

@@ -1,8 +1,15 @@
from django.shortcuts import render
from .services import list_scenarios
from .services import list_scenario_issues, list_scenarios
def index(request):
# 首页只消费服务层给出的场景摘要,不自行拼装配置字段。
return render(request, "scenarios/index.html", {"scenarios": list_scenarios()})
return render(
request,
"scenarios/index.html",
{
"scenarios": list_scenarios(),
"scenario_issues": list_scenario_issues(),
},
)

View File

@@ -9,6 +9,21 @@
<p class="page-lead">当前首页直接读取 YAML 场景配置。你可以从这里进入对话、上传资料,再用审计日志验证整条执行链路。</p>
</header>
{% if scenario_issues %}
<section class="panel" style="margin-bottom: 20px;">
<h2 class="section-title">配置异常</h2>
<p class="muted">以下 YAML 场景文件存在问题,系统已自动跳过,不会影响其它合法场景展示。</p>
<ul class="detail-list">
{% for issue in scenario_issues %}
<li class="detail-item">
<strong>{{ issue.file_name }}</strong>
<div>{{ issue.message }}</div>
</li>
{% endfor %}
</ul>
</section>
{% endif %}
<section class="card-grid">
{% for scenario in scenarios %}
<article class="card">

View File

@@ -3,6 +3,7 @@ import pytest
from apps.scenarios.services import (
ScenarioNotFound,
get_scenario,
list_scenario_issues,
list_scenarios,
)
@@ -38,3 +39,91 @@ def test_home_page_shows_applicable_questions(client):
assert response.status_code == 200
assert "适用题型" in content
assert "SOP 问答" in content
def test_list_scenarios_skips_invalid_config_and_collects_issues(settings, tmp_path):
valid_file = tmp_path / "valid.yaml"
invalid_file = tmp_path / "invalid.yaml"
valid_file.write_text(
"""
id: demo_valid
name: 有效场景
description: 用于测试
agent:
role: 测试助手
goal: 正常返回
instructions:
- 输出结果
rag:
enabled: false
tools: []
output:
type: general_answer
audit:
enabled: true
""".strip(),
encoding="utf-8",
)
invalid_file.write_text(
"""
id: broken
name: 非法场景
description: 缺少 agent.goal
agent:
role: 测试助手
instructions:
- 输出结果
rag:
enabled: true
tools: []
output:
type: general_answer
audit:
enabled: true
""".strip(),
encoding="utf-8",
)
settings.SCENARIO_CONFIG_DIR = tmp_path
scenarios = list_scenarios()
issues = list_scenario_issues()
assert [scenario["id"] for scenario in scenarios] == ["demo_valid"]
assert len(issues) == 1
assert issues[0]["file_name"] == "invalid.yaml"
assert "agent.goal" in issues[0]["message"]
def test_home_page_shows_invalid_scenario_issues_instead_of_500(client, settings, tmp_path):
valid_file = tmp_path / "valid.yaml"
invalid_file = tmp_path / "invalid.yaml"
valid_file.write_text(
"""
id: demo_valid
name: 有效场景
description: 用于测试
agent:
role: 测试助手
goal: 正常返回
instructions:
- 输出结果
rag:
enabled: false
tools: []
output:
type: general_answer
audit:
enabled: true
""".strip(),
encoding="utf-8",
)
invalid_file.write_text("id: broken\nname: 缺失结构", encoding="utf-8")
settings.SCENARIO_CONFIG_DIR = tmp_path
response = client.get("/")
content = response.content.decode("utf-8")
assert response.status_code == 200
assert "有效场景" in content
assert "配置异常" in content
assert "invalid.yaml" in content