feat(v2.5.0): Sec-Ch-Ua 头修复 + 结构化提取 + token 预算 + 正文去重 + 连接复用
正确性修复: - 修复 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 测试不失效)
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user