配置层: - 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
125 lines
3.8 KiB
Python
125 lines
3.8 KiB
Python
#!/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())
|