server.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. """
  2. FastAPI 服务层 - 把测试能力暴露成 HTTP 接口
  3. 让浏览器(前端)能通过网络调用我们的 Agent。
  4. 之前的 src/ 代码一行都不用改,直接复用。
  5. """
  6. import sys
  7. from pathlib import Path
  8. # Windows 中文控制台 GBK 坑
  9. if hasattr(sys.stdout, "reconfigure"):
  10. sys.stdout.reconfigure(encoding="utf-8", errors="replace")
  11. from fastapi import FastAPI, HTTPException
  12. from fastapi.middleware.cors import CORSMiddleware
  13. from fastapi.staticfiles import StaticFiles
  14. from pydantic import BaseModel
  15. from dotenv import load_dotenv
  16. # 关键:先加载 .env,再 import Agent
  17. load_dotenv()
  18. from src.agents.parser_agent import ParserAgent
  19. from src.agents.generator_agent import GeneratorAgent
  20. from src.agents.executor_agent import ExecutorAgent
  21. from src.agents.validator_agent import ValidatorAgent
  22. from src.agents.reporter_agent import ReporterAgent
  23. app = FastAPI(title="智能API测试助手")
  24. # CORS 配置:允许前端跨域调用(简单起见先放开所有来源)
  25. app.add_middleware(
  26. CORSMiddleware,
  27. allow_origins=["*"],
  28. allow_methods=["*"],
  29. allow_headers=["*"],
  30. )
  31. # 请求体模型:前端 POST 过来的数据结构
  32. class TestRequest(BaseModel):
  33. # 保留 openapi_text,兼容原来的前端和外部调用方;两者与 openapi_url 二选一
  34. openapi_text: str | None = None # OpenAPI 文档内容(文本)
  35. openapi_url: str | None = None # OpenAPI 文档地址(由后端抓取)
  36. base_url: str # 目标 API 基础地址
  37. # 全局请求头(可选),如 {"Authorization": "Bearer xxx"},
  38. # 用于给受保护接口传认证信息,会原样透传给 ExecutorAgent
  39. headers: dict[str, str] | None = None
  40. def _json_safe(value):
  41. """把执行内部使用的 multipart 二进制转换成可返回给前端的值。
  42. 文件内容必须以 bytes 传给 requests,但不能把原始 bytes 放进 FastAPI
  43. 响应;PNG 等二进制不是合法 UTF-8,jsonable_encoder 会因此抛异常。
  44. """
  45. if isinstance(value, (bytes, bytearray, memoryview)):
  46. return f"<binary data: {len(value)} bytes>"
  47. if isinstance(value, dict):
  48. return {key: _json_safe(item) for key, item in value.items()}
  49. if isinstance(value, (list, tuple)):
  50. return [_json_safe(item) for item in value]
  51. return value
  52. @app.post("/api/test")
  53. def test_api(req: TestRequest):
  54. """一键执行完整测试流程
  55. 接收前端传来的 {openapi_text, base_url} 或 {openapi_url, base_url},
  56. 依次调用 5 个 Agent,返回测试结果。
  57. """
  58. try:
  59. source_text = (req.openapi_text or "").strip()
  60. source_url = (req.openapi_url or "").strip()
  61. if bool(source_text) == bool(source_url):
  62. raise HTTPException(
  63. status_code=400,
  64. detail="请在 OpenAPI 文档内容和文档 URL 中二选一",
  65. )
  66. # ① 解析文档:文本模式沿用原逻辑,URL 模式复用已有 parse_url
  67. parser = ParserAgent()
  68. if source_url:
  69. endpoints = parser.parse_url(source_url)
  70. else:
  71. endpoints = parser.parse_text(source_text)
  72. # 文档里没有解析出任何接口,直接返回明确错误
  73. if not endpoints:
  74. source_hint = "URL" if source_url else "文档内容"
  75. raise HTTPException(
  76. status_code=400,
  77. detail=f"未能从 {source_hint} 解析出任何接口,请检查地址或内容是否正确",
  78. )
  79. # ② 生成用例
  80. generator = GeneratorAgent()
  81. all_cases = []
  82. for endpoint in endpoints:
  83. all_cases.extend(generator.generate(endpoint))
  84. # ③ 执行测试(把前端传来的认证头一并透传)
  85. executor = ExecutorAgent()
  86. execution_results = executor.execute(all_cases, req.base_url, headers=req.headers)
  87. # ④ 验证结果
  88. validator = ValidatorAgent()
  89. validated_results = validator.validate(execution_results)
  90. # ⑤ 统计汇总
  91. reporter = ReporterAgent()
  92. summary = reporter.summarize(validated_results)
  93. return {
  94. "summary": summary,
  95. # 内部结果可能包含 multipart 的 bytes,响应前统一转成 JSON 安全值。
  96. "results": _json_safe(validated_results),
  97. }
  98. except HTTPException:
  99. raise
  100. except Exception as e:
  101. # LLM 调用失败、网络异常等,返回 500 并给出友好提示,避免无信息崩溃
  102. raise HTTPException(status_code=500, detail=f"测试执行失败:{e}")
  103. # ===== 前端托管 =====
  104. # Vue3 工程每次 `npm run build` 会把打包产物生成到 frontend/dist/
  105. # 只托管 Vue 构建产物,不再回退到旧版单文件页面
  106. DIST_DIR = Path(__file__).resolve().parent / "frontend" / "dist"
  107. if DIST_DIR.exists():
  108. # 注意:挂载必须放在所有 /api 路由定义【之后】,FastAPI 按注册顺序匹配,
  109. # 前面的 /api/test 会先命中,静态目录只接管其余请求
  110. app.mount("/", StaticFiles(directory=DIST_DIR, html=True), name="frontend")
  111. else:
  112. @app.get("/")
  113. def index():
  114. """未构建 Vue 前端时返回明确提示"""
  115. raise HTTPException(
  116. status_code=503,
  117. detail="Vue 前端尚未构建,请先执行 cd frontend && npm run build",
  118. )
  119. if __name__ == "__main__":
  120. import uvicorn
  121. uvicorn.run(app, host="0.0.0.0", port=8000)