正确性修复: - 修复 Sec-Ch-Ua 构造 bug: 原实现产出 ""Not_A Brand";v="99"" 双重引号 畸形头(Chrome/Edge 两路径), 严格校验的 WAF 会忽略; 改为品牌数组拼接 - search 403 快速失败: 实例级 403 不再退避重试(~10.5s 空等), 立即 failover - AdaptiveThrottle: --throttle-failure-threshold 0 现为真正禁用语义 - number_of_results 缺失时用 len(results) 兜底(JSON/HTML 路径契约对齐) - 版本对齐: pyproject.toml 与 _config.py 同步 2.5.0 AI 代理体验: - fetch.py --extract json: 结构化骨架(title/meta/headings/links/images) - fetch.py --max-chars N: 提取后语义级截断(区别于 --max-size 字节截断) - search --fetch-total-chars N: --fetch 全局字符预算, 耗尽后 status=skipped - --dedup-fetched-content: 抓取正文 SimHash 去重, status=duplicate - --progress 新增 angle_start/ok/fail + fetch_skip/fetch_duplicate 事件 - fetch.py 补齐 --log-format json + --dump-schema - CSV 媒体列自适应(images/videos 类别自动追加媒体字段列) - --dry-run 批量模式打印实际查询列表 - search 连接复用: requests 可用时走模块级 Session(连接池) 工程治理: - 新增 scripts/release_check.py 发布一致性检查(版本/错误码表漂移) - 新增 tests/test_v250_features.py 46+4 个回归测试(全量 622 通过) - tests/conftest.py: autouse fixture 强制 stdlib 路径(本机有 requests 时 既有 urllib mock 测试不失效)
145 lines
5.2 KiB
Python
145 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""发布前一致性检查(v2.5.0)。
|
|
|
|
检查版本号与错误码表在多个文件间是否漂移——历史上 pyproject.toml 与
|
|
``_config.VERSION`` 曾不同步(v2.4.0 发布时前者停在 2.3.0)。发布前运行
|
|
本脚本即可一次性发现:
|
|
|
|
1. 版本一致性:pyproject.toml <-> _config.py(同一来源),并警告
|
|
README.md / SKILL.md 中提到的版本号是否与最新版本一致(文档是
|
|
手工维护的,仅警告不阻断)
|
|
2. 错误码一致性:common.py 的 ``E_*`` 常量 vs 各脚本实际使用 vs
|
|
RECOVERY_HINTS 覆盖 vs README/SKILL 错误码表中提及的错误码
|
|
|
|
用法(项目根目录)::
|
|
|
|
python scripts/release_check.py # 完整检查
|
|
python scripts/release_check.py --strict # 文档警告也视为失败
|
|
|
|
零依赖(stdlib),退出码 0=通过 / 1=有阻断问题。
|
|
"""
|
|
|
|
import argparse
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
SCRIPTS_DIR = ROOT / "scripts"
|
|
|
|
# 文档中可能提及版本号的锚点模式(供提示)
|
|
_VERSION_HINT_RE = re.compile(r"\b\d+\.\d+\.\d+\b")
|
|
|
|
# 错误码表(README.md 与 SKILL.md 均有)应包含的 E_* 代码,从 common.py
|
|
# 动态提取,与源码单一来源保持一致。捕获组只取标识符本身(不含 = ")。
|
|
_ERROR_CODE_RE = re.compile(r"^\s*(E_[A-Z_]+)\s*=\s*[\"']", re.MULTILINE)
|
|
|
|
|
|
def _read(path: Path) -> str:
|
|
try:
|
|
return path.read_text(encoding="utf-8")
|
|
except OSError as e:
|
|
print(f" [WARN] 无法读取 {path}: {e}")
|
|
return ""
|
|
|
|
|
|
def check_version(strict: bool) -> list:
|
|
"""检查版本号一致性。返回错误消息列表(空 = 通过)。"""
|
|
errors = []
|
|
|
|
# 1. pyproject.toml 的 version 字段
|
|
pyproject = ROOT / "pyproject.toml"
|
|
pp = _read(pyproject)
|
|
m = re.search(r"^version\s*=\s*[\"']([\d.]+)[\"']", pp, re.MULTILINE)
|
|
pp_version = m.group(1) if m else None
|
|
|
|
# 2. _config.py 的 VERSION(权威来源)
|
|
import importlib.util
|
|
spec = importlib.util.spec_from_file_location("_config", SCRIPTS_DIR / "_config.py")
|
|
cfg = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(cfg)
|
|
cfg_version = cfg.VERSION
|
|
|
|
if pp_version and pp_version != cfg_version:
|
|
errors.append(
|
|
f"版本漂移: pyproject.toml={pp_version} 但 _config.VERSION={cfg_version}。"
|
|
f"请同步 pyproject.toml 或改为从 _config 动态读取。"
|
|
)
|
|
elif not pp_version:
|
|
errors.append("pyproject.toml 缺少 version 字段。")
|
|
|
|
# 3. 文档版本提示(仅警告)
|
|
if cfg_version:
|
|
for doc_name in ("README.md", "SKILL.md"):
|
|
doc = _read(ROOT / doc_name)
|
|
if not doc:
|
|
continue
|
|
mentioned = set(_VERSION_HINT_RE.findall(doc))
|
|
# 文档可能同时提到旧版本历史(changelog 里 v2.3.0 等),
|
|
# 只要"存在"当前版本号即视为已更新
|
|
if cfg_version not in mentioned:
|
|
msg = (f"文档 {doc_name} 未提及当前版本 {cfg_version} "
|
|
f"(提到: {sorted(mentioned)[:6]}...)。请更新文档。")
|
|
if strict:
|
|
errors.append(msg)
|
|
else:
|
|
print(f" [WARN] {msg}")
|
|
return errors
|
|
|
|
|
|
def check_error_codes(strict: bool) -> list:
|
|
"""检查错误码表一致性。返回错误消息列表(空 = 通过)。"""
|
|
errors = []
|
|
|
|
common_src = _read(SCRIPTS_DIR / "common.py")
|
|
codes = _ERROR_CODE_RE.findall(common_src)
|
|
if not codes:
|
|
errors.append("common.py 中未找到 E_* 常量定义。")
|
|
return errors
|
|
codes = sorted(codes)
|
|
|
|
# 1. 每个 E_* 是否有 recovery_hint
|
|
hint_keys = set(re.findall(r'^\s*"?(E_[A-Z_]+)"?\s*:\s*[\(\["\']',
|
|
common_src, re.MULTILINE))
|
|
missing_hints = [c for c in codes if c not in hint_keys]
|
|
if missing_hints:
|
|
errors.append(f"缺少 RECOVERY_HINTS 的错误码: {missing_hints}")
|
|
|
|
# 2. README/SKILL 错误码表是否覆盖所有 E_*(文档漂移提示)
|
|
for doc_name in ("README.md", "SKILL.md"):
|
|
doc = _read(ROOT / doc_name)
|
|
if not doc:
|
|
continue
|
|
missing_doc = [c for c in codes if c not in doc]
|
|
if missing_doc:
|
|
msg = (f"文档 {doc_name} 错误码表缺少: {missing_doc}。"
|
|
f"源码已新增这些错误码,请更新文档表格。")
|
|
if strict:
|
|
errors.append(msg)
|
|
else:
|
|
print(f" [WARN] {msg}")
|
|
return errors
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description="searxng-cli 发布前一致性检查")
|
|
ap.add_argument("--strict", action="store_true",
|
|
help="文档警告也视为失败(默认仅提示)")
|
|
args = ap.parse_args()
|
|
|
|
errors = []
|
|
errors += check_version(args.strict)
|
|
errors += check_error_codes(args.strict)
|
|
|
|
if errors:
|
|
print("✗ 检查未通过:")
|
|
for e in errors:
|
|
print(f" - {e}")
|
|
return 1
|
|
print("✓ 版本号与错误码一致性检查全部通过")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|