http_client.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. """
  2. HTTP 请求工具
  3. 负责真正发送 HTTP 请求,并统一处理超时、重试和错误。
  4. 这是"工具层"——没有大脑,只会老老实实发请求、拿结果。
  5. """
  6. import time
  7. import requests
  8. from src.config import REQUEST_TIMEOUT, REQUEST_MAX_RETRIES
  9. class HttpClient:
  10. """HTTP 客户端工具
  11. 封装 requests 库,提供:
  12. - 统一的超时控制
  13. - 失败自动重试
  14. - 标准化的返回结果(方便后面的验证Agent使用)
  15. """
  16. # 支持的 HTTP 方法列表
  17. SUPPORTED_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH"]
  18. def __init__(self, timeout=REQUEST_TIMEOUT, max_retries=REQUEST_MAX_RETRIES):
  19. """
  20. 初始化工具
  21. Args:
  22. timeout: 单次请求超时时间(秒),默认从 config 读取
  23. max_retries: 失败后重试次数,默认从 config 读取
  24. """
  25. self.timeout = timeout
  26. self.max_retries = max_retries
  27. def request(self, method, url, headers=None, params=None, body=None,
  28. files=None, content_type="application/json"):
  29. """发送 HTTP 请求(带自动重试)
  30. Args:
  31. method: HTTP 方法,如 "GET"、"POST"
  32. url: 完整的请求地址
  33. headers: 请求头字典,如 {"Authorization": "Bearer xxx"}
  34. params: 查询参数(URL 中 ? 后面的部分)
  35. body: 请求体(JSON 时的 dict,或表单字段)
  36. files: 上传的文件,格式 {"字段名": ("文件名", 字节内容, "MIME")},传了就走 multipart
  37. content_type: 请求体媒体类型,如 "application/json" / "multipart/form-data"
  38. Returns:
  39. 标准化结果字典:
  40. {
  41. "success": bool, # 是否成功
  42. "status_code": int, # 状态码,如 200、404
  43. "body": dict 或 str, # 解析后的响应体
  44. "elapsed": float, # 耗时(秒)
  45. "error": str 或 None # 错误信息(成功时为 None)
  46. }
  47. """
  48. method = method.upper()
  49. # 检查方法是否支持
  50. if method not in self.SUPPORTED_METHODS:
  51. return self._error_result(f"不支持的 HTTP 方法: {method}")
  52. # 带重试的请求循环:最多试 (max_retries + 1) 次
  53. last_error = None
  54. for attempt in range(self.max_retries + 1):
  55. try:
  56. return self._do_request(method, url, headers, params, body, files, content_type)
  57. except requests.RequestException as e:
  58. last_error = e
  59. # 如果不是最后一次,休息 1 秒再重试
  60. if attempt < self.max_retries:
  61. time.sleep(1)
  62. # 所有重试都失败,返回错误结果
  63. return self._error_result(f"请求失败(已重试 {self.max_retries} 次): {last_error}")
  64. def _do_request(self, method, url, headers, params, body, files, content_type):
  65. """真正执行一次请求(不重试,被 request 方法调用)
  66. 根据请求体类型选择序列化方式:
  67. - 有 files:multipart 文件上传(body 作为表单字段,files 作为文件字段)
  68. - content_type 是 multipart:纯表单字段(无文件)
  69. - 其它:JSON 请求体
  70. """
  71. start = time.time()
  72. kwargs = {
  73. "method": method,
  74. "url": url,
  75. "headers": headers,
  76. "params": params,
  77. "timeout": self.timeout,
  78. }
  79. if files is not None:
  80. # multipart/form-data:文件字段走 files,普通字段走 data
  81. kwargs["data"] = body or {}
  82. kwargs["files"] = files
  83. elif content_type and "multipart" in content_type.lower():
  84. # multipart 但没有文件(纯表单字段)
  85. kwargs["data"] = body or {}
  86. elif body is not None:
  87. # 默认 JSON 请求体,自动把 dict 转成 JSON 格式
  88. kwargs["json"] = body
  89. response = requests.request(**kwargs)
  90. elapsed = time.time() - start
  91. return {
  92. "success": True,
  93. "status_code": response.status_code,
  94. "body": self._parse_body(response),
  95. "elapsed": round(elapsed, 3),
  96. "error": None,
  97. }
  98. def _parse_body(self, response):
  99. """把响应体解析成 dict(如果是 JSON)或字符串"""
  100. try:
  101. return response.json()
  102. except ValueError:
  103. return response.text
  104. def _error_result(self, message):
  105. """构造一个标准的"失败"结果字典"""
  106. return {
  107. "success": False,
  108. "status_code": None,
  109. "body": None,
  110. "elapsed": 0.0,
  111. "error": message,
  112. }