feat: 可配置报告语言(默认 zh-CN) #3

Merged
layue13 merged 3 commits from cursor/report-language-zh-165a into main 2026-10-27 09:00:42 +00:00
Showing only changes of commit f3b62481b0 - Show all commits
+79 -44
View File
@@ -118,16 +118,19 @@ def secret_value(key_name: str) -> str:
return ""
def http_json(url: str, headers: dict) -> dict | list:
def http_json(url: str, headers: dict, timeout: float = 25) -> dict | list:
req = urllib.request.Request(url, headers=headers, method="GET")
with urllib.request.urlopen(req, timeout=60) as resp:
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode("utf-8"))
def normalize_protocol(p: str) -> str:
p = (p or "").strip().lower()
if p not in PROTOCOL_WIRE:
die(f"unknown protocol {p!r} (want {' | '.join(PROTOCOLS)})")
die(
f"unknown protocol {p!r} (want {' | '.join(PROTOCOLS)})",
msg_zh=f"未知 protocol {p!r}(可选 {' | '.join(PROTOCOLS)})",
)
return p
@@ -135,41 +138,72 @@ def wire_protocol(p: str) -> str:
return PROTOCOL_WIRE[normalize_protocol(p)]
def discover_model_ids(base_url: str, protocol: str, api_key: str) -> set[str]:
def discover_model_ids(base_url: str, protocol: str, api_key: str) -> set[str] | None:
"""Return model id set, or None if discovery failed after retries (skip validation)."""
import time
base = base_url.rstrip("/")
wire = wire_protocol(protocol)
ids: set[str] = set()
try:
if wire in ("openai-completions", "openai-responses"):
data = http_json(
f"{base}/models",
{"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
if wire not in ("openai-completions", "openai-responses", "anthropic"):
die(f"unhandled protocol wire {wire!r}")
last_err: Exception | None = None
attempts = int(os.environ.get("GATEKEEP_DISCOVER_RETRIES", "3"))
for attempt in range(max(1, attempts)):
try:
ids: set[str] = set()
if wire in ("openai-completions", "openai-responses"):
data = http_json(
f"{base}/models",
{
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
)
for m in data.get("data") or []:
mid = m.get("id")
if mid:
ids.add(mid)
else:
data = http_json(
f"{base}/v1/models",
{
"x-api-key": api_key,
"anthropic-version": "2023-06-01",
"Content-Type": "application/json",
},
)
for m in data.get("data") or []:
mid = m.get("id")
if mid:
ids.add(mid)
return ids
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", errors="replace")[:400]
last_err = RuntimeError(f"HTTP {e.code} {body}")
# 401/403 are configuration errors — fail closed
if e.code in (401, 403):
die(
f"model discovery failed for {base}: {last_err}",
msg_zh=f"拉取模型列表失败({base}):{last_err} — 请检查 key",
)
except Exception as e:
last_err = e
if attempt + 1 < attempts:
sleep_s = 2**attempt
print(
f"gatekeep: model discovery retry {attempt + 1}/{attempts} "
f"after {sleep_s}s ({last_err})",
file=sys.stderr,
)
for m in data.get("data") or []:
mid = m.get("id")
if mid:
ids.add(mid)
elif wire == "anthropic":
data = http_json(
f"{base}/v1/models",
{
"x-api-key": api_key,
"anthropic-version": "2023-06-01",
"Content-Type": "application/json",
},
)
for m in data.get("data") or []:
mid = m.get("id")
if mid:
ids.add(mid)
else:
die(f"unhandled protocol wire {wire!r}")
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", errors="replace")[:400]
die(f"model discovery failed for {base}: HTTP {e.code} {body}")
except Exception as e:
die(f"model discovery failed for {base}: {e}")
return ids
time.sleep(sleep_s)
print(
f"gatekeep: model discovery failed for {base} after {attempts} tries "
f"({last_err}); skipping name validation and continuing",
file=sys.stderr,
)
return None
def enrich_provider(pid: str, pconf: dict) -> dict:
@@ -198,6 +232,12 @@ def main() -> None:
override_csv = sys.argv[3] if len(sys.argv) > 3 else ""
cfg = load_raw(cfg_path)
# Resolve language early so die() can speak Chinese when configured
override_lang = (os.environ.get("GATEKEEP_LANGUAGE") or "").strip()
cfg_lang = str(cfg.get("language") or cfg.get("report_language") or "").strip()
language = override_lang or cfg_lang or "en"
os.environ["GATEKEEP_LANGUAGE"] = language
providers_in = cfg["providers"]
providers: dict[str, dict] = {
pid: enrich_provider(pid, pconf) for pid, pconf in providers_in.items()
@@ -278,7 +318,7 @@ def main() -> None:
),
)
available = discover_model_ids(pconf["base_url"], pconf["protocol"], api_key)
if not available:
if available is not None and len(available) == 0:
die(
f"provider {pid}: model list empty — check base_url/protocol/key",
msg_zh=f"供应商 {pid} 模型列表为空 — 请检查 base_url / protocol / key",
@@ -289,14 +329,14 @@ def main() -> None:
"wire_protocol": wire_protocol(pconf["protocol"]),
"key_name": key_name,
"api_key": api_key,
"available_count": len(available),
"_available": available,
"available_count": len(available) if available is not None else -1,
"_available": available, # None ⇒ skip validation
}
def to_plan_entry(r: dict) -> dict:
pid, model = r["provider"], r["model"]
avail = provider_meta[pid]["_available"]
if model not in avail:
if avail is not None and model not in avail:
sample = sorted(avail)[:8]
die(
f"model {model!r} not available on provider {pid} "
@@ -324,11 +364,6 @@ def main() -> None:
)
os.chmod(secrets_path, 0o600)
# Action input GATEKEEP_LANGUAGE overrides YAML; empty → YAML; missing → en
override_lang = (os.environ.get("GATEKEEP_LANGUAGE") or "").strip()
cfg_lang = str(cfg.get("language") or cfg.get("report_language") or "").strip()
language = override_lang or cfg_lang or "en"
plan = {
"agreement_min": int(cfg.get("agreement_min", 2)),
"temperature": cfg.get("temperature", 0),