feat: 结构化输出按官方规范 + 两段式重试 + DeepSeek 复检开关

- 豆包: response_format.json_schema(strict);DeepSeek: 切 Responses API text.format.json_schema
  (官方 chat/completions 通道不支持 json_schema),不支持时自动降级 json_object([output] schema)
- 重试两段式:传输错误 cfg.retries 次;解析失败/空正文有独立 3 次专用重试,仍失败落 parse_fail
- 移除三票复核(decide_final/_verify_clean/verify_clean),DeepSeek 单次判定即终稿
- 新增 [run] deepseek_recheck 开关(默认 no=仅豆包初筛;yes=追加 DeepSeek 复检)
- GUI 不再覆盖 config 提示词;prompt 相对/前导斜杠路径按 exe 目录解析
- 报表去「票型/复核」列并同步说明;README/config.example.ini 同步
- 测试新增输出格式、重试、开关与提示词路径用例(50 passed)
This commit is contained in:
2026-09-03 11:44:10 +08:00
parent ae3a4e06f1
commit b413104587
12 changed files with 832 additions and 289 deletions
+70 -11
View File
@@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-
"""config 模块测试:模板生成、缺项检测、读写回环、提示词路径。"""
import violation_detector.config as config_mod
from violation_detector.config import (
AppConfig, ensure_config, load_config, missing_fields, save_config,
AppConfig, effective_mode, ensure_config, load_config, missing_fields,
save_config,
)
@@ -63,18 +65,57 @@ def test_mode_from_ini(tmp_path):
assert load_config(str(ini)).mode == "cascade"
def test_verify_clean_from_ini(tmp_path):
def test_output_schema_from_ini(tmp_path):
ini = tmp_path / "config.ini"
ini.write_text("[run]\nverify_clean = no\n", encoding="utf-8")
assert load_config(str(ini)).verify_clean is False
ini.write_text("[run]\nverify_clean = yes\n", encoding="utf-8")
assert load_config(str(ini)).verify_clean is True
# 非法值保持默认开启
ini.write_text("[run]\nverify_clean = 也许\n", encoding="utf-8")
assert load_config(str(ini)).verify_clean is True
# 未配置保持默认开启
ini.write_text("[output]\nschema = on\n", encoding="utf-8")
assert load_config(str(ini)).output_schema_mode == "on"
ini.write_text("[output]\nschema = off\n", encoding="utf-8")
assert load_config(str(ini)).output_schema_mode == "off"
ini.write_text("[output]\nschema = auto\n", encoding="utf-8")
assert load_config(str(ini)).output_schema_mode == "auto"
# 非法值回退 auto;未配置默认 auto
ini.write_text("[output]\nschema = 乱来\n", encoding="utf-8")
assert load_config(str(ini)).output_schema_mode == "auto"
ini.write_text("[ark]\n", encoding="utf-8")
assert load_config(str(ini)).verify_clean is True
assert load_config(str(ini)).output_schema_mode == "auto"
def test_output_schema_roundtrip(tmp_path):
ini = tmp_path / "config.ini"
save_config(AppConfig(output_schema_mode="off"), str(ini))
assert load_config(str(ini)).output_schema_mode == "off"
assert AppConfig().output_schema_mode == "auto"
def test_deepseek_recheck_default_and_ini(tmp_path):
# 默认关
assert AppConfig().deepseek_recheck is False
ini = tmp_path / "config.ini"
ini.write_text("[run]\ndeepseek_recheck = yes\n", encoding="utf-8")
assert load_config(str(ini)).deepseek_recheck is True
ini.write_text("[run]\ndeepseek_recheck = no\n", encoding="utf-8")
assert load_config(str(ini)).deepseek_recheck is False
# 非法值/未配置保持默认关
ini.write_text("[run]\ndeepseek_recheck = 也许\n", encoding="utf-8")
assert load_config(str(ini)).deepseek_recheck is False
ini.write_text("[ark]\n", encoding="utf-8")
assert load_config(str(ini)).deepseek_recheck is False
def test_effective_mode_respects_recheck_switch():
on = AppConfig(mode="cascade", deepseek_recheck=True)
off = AppConfig(mode="cascade", deepseek_recheck=False)
assert effective_mode(on, "cascade") == "cascade"
assert effective_mode(off, "cascade") == "doubao"
# 开关只影响 cascadedoubao/deepseek 不受影响
assert effective_mode(off, "doubao") == "doubao"
assert effective_mode(off, "deepseek") == "deepseek"
def test_deepseek_recheck_roundtrip(tmp_path):
ini = tmp_path / "config.ini"
save_config(AppConfig(deepseek_recheck=True), str(ini))
assert load_config(str(ini)).deepseek_recheck is True
def test_save_and_load_roundtrip(tmp_path):
@@ -104,3 +145,21 @@ def test_prompt_path_custom(tmp_path):
p.write_text("x", encoding="utf-8")
cfg = AppConfig(prompt_file=str(p))
assert cfg.prompt_path == p
def test_prompt_path_relative_prefers_app_dir(tmp_path, monkeypatch):
(tmp_path / "prompts.txt").write_text("x", encoding="utf-8")
monkeypatch.setattr(config_mod, "app_dir", lambda: tmp_path)
monkeypatch.setattr(config_mod, "resource_dir", lambda: tmp_path)
cfg = AppConfig(prompt_file="prompts.txt")
assert cfg.prompt_path == tmp_path / "prompts.txt"
def test_prompt_path_leading_slash_treated_relative(tmp_path, monkeypatch):
# 无盘符的 /prompts.txt 应视为 exe 同目录的相对文件(Windows 容错)
(tmp_path / "prompts.txt").write_text("x", encoding="utf-8")
monkeypatch.setattr(config_mod, "app_dir", lambda: tmp_path)
monkeypatch.setattr(config_mod, "resource_dir", lambda: tmp_path)
for raw in ("/prompts.txt", "\\prompts.txt"):
cfg = AppConfig(prompt_file=raw)
assert cfg.prompt_path == tmp_path / "prompts.txt", raw