export_openapi.py 3.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. import argparse
  2. import json
  3. from pathlib import Path
  4. from zhifu.app import create_app
  5. from zhifu.modules.knowledge.inference import app as inference_app
  6. from zhifu.monitoring.api import app as operations_app
  7. from zhifu.oms.api import create_app as create_oms
  8. ROOT = Path(__file__).resolve().parents[1]
  9. OUTPUT = ROOT / "docs" / "api"
  10. METHODS = {"get", "post", "put", "patch", "delete", "head", "options"}
  11. def specifications():
  12. # 只创建应用与 OpenAPI 定义,不启动 lifespan,也不连接数据库或加载模型权重。
  13. return {
  14. "openapi.json": create_app().openapi(),
  15. "oms-openapi.json": create_oms().openapi(),
  16. "inference-openapi.json": inference_app.openapi(),
  17. "operations-openapi.json": operations_app.openapi(),
  18. }
  19. def catalog(documents):
  20. lines = [
  21. "# 接口目录",
  22. "",
  23. "由接口定义生成,更新命令:`uv run python scripts/export_openapi.py`。",
  24. "",
  25. ]
  26. for filename, document in documents.items():
  27. lines.extend(
  28. [
  29. f"## {document['info']['title']}",
  30. "",
  31. f"字段、参数、响应及错误说明见 [{filename}]({filename})。",
  32. "",
  33. "| 方法 | 路径 | 用途 | 认证 | 成功状态 |",
  34. "|---|---|---|---|---|",
  35. ]
  36. )
  37. for path, methods in document["paths"].items():
  38. for method, operation in methods.items():
  39. if method not in METHODS:
  40. continue
  41. success = ", ".join(code for code in operation["responses"] if code.startswith("2"))
  42. schemes = {
  43. scheme
  44. for requirement in operation.get("security", [])
  45. for scheme in requirement
  46. }
  47. # 渠道使用各自签名协议,目录与 OpenAPI security 保持一致。
  48. security = (
  49. "Standard Webhooks" if "StandardWebhooks" in schemes
  50. else "HMAC" if "ChannelHMAC" in schemes
  51. else "Bearer" if schemes else "无"
  52. )
  53. lines.append(
  54. f"| {method.upper()} | `{path}` | {operation['summary']} "
  55. f"| {security} | {success} |"
  56. )
  57. lines.append("")
  58. return "\n".join(lines)
  59. def main(check=False):
  60. documents = specifications()
  61. artifacts = {
  62. name: json.dumps(document, ensure_ascii=False, indent=2) + "\n"
  63. for name, document in documents.items()
  64. }
  65. artifacts["routes.md"] = catalog(documents)
  66. for name, content in artifacts.items():
  67. destination = OUTPUT / name
  68. if check:
  69. if destination.read_text(encoding="utf-8") != content:
  70. raise RuntimeError(f"接口文档需要更新:{name}")
  71. else:
  72. destination.write_text(content, encoding="utf-8")
  73. print(f"{'checked' if check else 'exported'}: {name}")
  74. counts = {
  75. name: sum(method in METHODS for methods in document["paths"].values() for method in methods)
  76. for name, document in documents.items()
  77. }
  78. print(json.dumps(counts))
  79. if __name__ == "__main__":
  80. parser = argparse.ArgumentParser(description="导出或核对四类服务的接口文档")
  81. parser.add_argument("--check", action="store_true")
  82. main(parser.parse_args().check)