Files
gaokao-volunteer-system/scripts/payment_readiness_doctor.py
Hermes Agent 43c1dbdf32
Some checks failed
CI / pytest (Python 3.10) (push) Has been cancelled
CI / pytest (Python 3.11) (push) Has been cancelled
CI / pytest (Python 3.12) (push) Has been cancelled
feat(llm): 内置 LLM 供应商配置与审核主链接入骨架
配置层:
- Settings 新增 llm_provider/api_key/base_url/model/timeout/max_tokens
- 生产 fail-closed: GAOKAO_LLM_PROVIDER=none 禁止, provider!=none 且 API key 为空禁止
- .env.docker.example / .env.payment.example 补 LLM 变量
- payment_readiness_doctor 将 LLM 配置纳入 readiness 检查

基础设施:
- 新增 data/llm/client.py: OpenAI-compatible LLMClient
- 新增 data/llm/prompts.py: audit/cwb/full_plan prompt 模板
- 新增 data/llm/tests/test_llm.py: 12 个单元测试

主链接入:
- ReviewResultContract 新增 llm_generated / llm_summary / llm_cwb_suggestions
- _start_review_result 优先尝试 LLM 生成审核结果, 失败时回退到原规则默认逻辑
- cwb 页面优先展示 LLM 生成的三档建议

测试适配:
- conftest / health / app / p2_4 tests 注入默认 LLM 测试配置,避免被新 fail-closed 提前拦截

验证:
- data/llm/tests 12 passed
- 核心 prod settings tests 62 passed
2026-06-28 12:11:43 +08:00

125 lines
3.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""支付 provider readiness doctor.
检查真实支付 acceptance 所需的全部前置条件是否就绪。
返回 ready / missing_env_vars / missing_files / notes。
用法:
python3 scripts/payment_readiness_doctor.py
python3 scripts/payment_readiness_doctor.py --json
"""
from __future__ import annotations
import json
import os
import sys
from pathlib import Path
REQUIRED_ENV_VARS = [
"GAOKAO_LLM_PROVIDER",
"GAOKAO_LLM_API_KEY",
"GAOKAO_LLM_BASE_URL",
"GAOKAO_LLM_MODEL",
"GAOKAO_PAYMENT_APP_ID",
"GAOKAO_PAYMENT_MERCHANT_ID",
"GAOKAO_PAYMENT_PRIVATE_KEY_PATH",
"GAOKAO_PAYMENT_ALIPAY_PUBLIC_KEY_PATH",
"GAOKAO_PAYMENT_NOTIFY_URL",
"GAOKAO_PAYMENT_RETURN_URL",
"GAOKAO_PAYMENT_WEBHOOK_SECRET",
"GAOKAO_ORDERS_FERNET_KEY",
"GAOKAO_JWT_SECRET",
"GAOKAO_PORTAL_TOKEN_SECRET",
"GAOKAO_ADMIN_PASS",
]
REQUIRED_FILE_ENV_VARS = [
("GAOKAO_PAYMENT_PRIVATE_KEY_PATH", "应用私钥文件"),
("GAOKAO_PAYMENT_ALIPAY_PUBLIC_KEY_PATH", "支付宝公钥文件"),
]
REQUIRED_URL_PREFIX = "https://"
def check() -> dict:
missing_env = []
notes = []
for var in REQUIRED_ENV_VARS:
val = os.environ.get(var, "")
if not val:
missing_env.append(var)
# 检查 URL 是否 HTTPS
notify_url = os.environ.get("GAOKAO_PAYMENT_NOTIFY_URL", "")
if notify_url and not notify_url.startswith(REQUIRED_URL_PREFIX):
notes.append(
f"GAOKAO_PAYMENT_NOTIFY_URL 应为 HTTPS当前: {notify_url[:30]}..."
)
return_url = os.environ.get("GAOKAO_PAYMENT_RETURN_URL", "")
if return_url and not return_url.startswith(REQUIRED_URL_PREFIX):
notes.append(
f"GAOKAO_PAYMENT_RETURN_URL 应为 HTTPS当前: {return_url[:30]}..."
)
# JWT secret 长度
jwt = os.environ.get("GAOKAO_JWT_SECRET", "")
if jwt and len(jwt) < 32:
notes.append(f"GAOKAO_JWT_SECRET 长度不足 32当前 {len(jwt)}")
# Portal token secret 与 JWT 不同
portal = os.environ.get("GAOKAO_PORTAL_TOKEN_SECRET", "")
if jwt and portal and jwt == portal:
notes.append("GAOKAO_PORTAL_TOKEN_SECRET 与 GAOKAO_JWT_SECRET 相同,必须分离")
# 检查密钥文件是否存在
missing_files = []
for var, desc in REQUIRED_FILE_ENV_VARS:
path = os.environ.get(var, "")
if path and not Path(path).is_file():
missing_files.append({"var": var, "path": path, "desc": desc})
ready = len(missing_env) == 0 and len(missing_files) == 0 and len(notes) == 0
return {
"ready": ready,
"missing_env_vars": missing_env,
"missing_files": missing_files,
"notes": notes,
"total_required": len(REQUIRED_ENV_VARS),
"total_satisfied": len(REQUIRED_ENV_VARS) - len(missing_env),
}
def main() -> int:
as_json = "--json" in sys.argv
result = check()
if as_json:
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
status = "READY" if result["ready"] else "NOT READY"
print(f"[payment-doctor] {status}")
print(
f" env vars satisfied: {result['total_satisfied']}/{result['total_required']}"
)
if result["missing_env_vars"]:
print(f" missing env vars: {', '.join(result['missing_env_vars'])}")
if result["missing_files"]:
for f in result["missing_files"]:
print(f" missing file: {f['var']}{f['path']} ({f['desc']})")
if result["notes"]:
for n in result["notes"]:
print(f" note: {n}")
if not result["ready"]:
print("\n 参考 .env.payment.example 获取完整配置模板")
print(" 参考 docs/PAYMENT_PROVIDER_ONBOARDING.md 获取接入步骤")
return 0 if result["ready"] else 1
if __name__ == "__main__":
sys.exit(main())