"""task-166 HTTP 客户端契约盘点工具。 盘点 backend-java 各 HTTP 客户端(OSS/RustFS/图片下载/紫鸟/LLM)的现状: 超时(connect/read/write/call)、重试、幂等性,产出机器可读契约表供审计文档使用。 本工具只读,不改代码。 """ from __future__ import annotations import argparse import json import re import sys from dataclasses import dataclass, field from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent CONFIG_ROOT = REPO_ROOT / "src/main/java/com/nanri/aiimage/config" MAIN_ROOT = REPO_ROOT / "src/main/java" # 超时/重试配置字段(config 包) TIMEOUT_FIELD_RE = re.compile( r"private\s+\w+\s+(\w*(?:Timeout|Retries|Retry|MaxRetries)\w*)\s*=\s*([^;]+);", re.IGNORECASE, ) # 代码中 HttpClient/Client 构建的超时/重试配置 CLIENT_CONF_RE = re.compile( r"(connectTimeout|readTimeout|writeTimeout|callTimeout|operationTimeout|" r"maxRetries|retry|setConnectTimeout|setReadTimeout|HttpClient\.newBuilder)", re.IGNORECASE, ) # 客户端类识别 CLIENT_CLASS_RE = re.compile( r"(OssStorageService|RustfsObjectStorageService|ZiniaoShopSwitchService|" r"SimilarAsinLlmService|AppearancePatentLlmClient|ImageDownloader|ImageDownload|" r"imageDownload|HttpClient|WebClient|RestTemplate)", ) def extract_timeout_configs() -> list[dict]: """从 config 包提取超时/重试配置字段。""" entries: list[dict] = [] for java_file in sorted(CONFIG_ROOT.rglob("*.java")): text = java_file.read_text(encoding="utf-8") for match in TIMEOUT_FIELD_RE.finditer(text): entries.append( { "config_class": java_file.stem, "field": match.group(1), "default": match.group(2).strip(), "source": str(java_file.relative_to(REPO_ROOT)).replace("\\", "/"), } ) return entries def extract_client_sites() -> list[dict]: """扫描 main 源码中客户端构建/配置调用点。""" sites: list[dict] = [] for java_file in sorted(MAIN_ROOT.rglob("*.java")): text = java_file.read_text(encoding="utf-8") for line_no, line in enumerate(text.splitlines(), start=1): if not CLIENT_CONF_RE.search(line) or "import " in line: continue if CLIENT_CLASS_RE.search(line) or "Timeout" in line or "Retry" in line: sites.append( { "file": str(java_file.relative_to(REPO_ROOT)).replace( "\\", "/" ), "line": line_no, "snippet": line.strip()[:110], } ) return sites @dataclass class Report: timeout_configs: list[dict] = field(default_factory=list) client_sites: list[dict] = field(default_factory=list) def to_dict(self) -> dict: return { "timeout_configs": self.timeout_configs, "client_sites": self.client_sites, } def scan() -> Report: return Report( timeout_configs=extract_timeout_configs(), client_sites=extract_client_sites() ) def main() -> None: parser = argparse.ArgumentParser(description="HTTP 客户端契约盘点") parser.add_argument("--json", help="输出 JSON 报告路径(默认 stdout)") args = parser.parse_args() report = scan() payload = report.to_dict() if args.json: Path(args.json).write_text( json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8" ) else: print(json.dumps(payload, ensure_ascii=False, indent=2)) print( f"timeout configs: {len(payload['timeout_configs'])}, " f"client sites: {len(payload['client_sites'])}", file=sys.stderr, ) if __name__ == "__main__": main()