utils.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. import re
  2. import time
  3. import hashlib
  4. from typing import Dict, List, Optional, Tuple
  5. from functools import wraps
  6. from flask import request, jsonify
  7. from http import HTTPStatus
  8. from datetime import datetime, timezone, timedelta
  9. def get_beijing_time() -> datetime:
  10. """
  11. 获取北京时间(UTC+8)
  12. Returns:
  13. 北京时间的datetime对象
  14. """
  15. beijing_tz = timezone(timedelta(hours=8))
  16. return datetime.now(beijing_tz)
  17. def sanitize_input(text: str, max_length: int = 20000) -> Tuple[str, Optional[str]]:
  18. """
  19. 清理和验证用户输入
  20. Args:
  21. text: 输入文本
  22. max_length: 最大长度限制
  23. Returns:
  24. (清理后的文本, 错误信息)
  25. """
  26. if not text or not text.strip():
  27. return "", "消息不能为空"
  28. # 移除首尾空白
  29. text = text.strip()
  30. # 检查长度
  31. if len(text) > max_length:
  32. return "", f"消息长度不能超过{max_length}字符"
  33. # 移除潜在的恶意脚本标签
  34. text = re.sub(r'<script[^>]*>.*?</script>', '', text, flags=re.IGNORECASE | re.DOTALL)
  35. text = re.sub(r'<iframe[^>]*>.*?</iframe>', '', text, flags=re.IGNORECASE | re.DOTALL)
  36. # 限制连续换行
  37. text = re.sub(r'\n{3,}', '\n\n', text)
  38. return text, None
  39. def validate_user_id(user_id: str) -> Tuple[str, Optional[str]]:
  40. """
  41. 验证用户ID
  42. Args:
  43. user_id: 用户ID
  44. Returns:
  45. (验证后的用户ID, 错误信息)
  46. """
  47. if not user_id:
  48. return "", "用户ID不能为空"
  49. # 检查用户ID格式(UUID格式)
  50. uuid_pattern = re.compile(
  51. r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
  52. re.IGNORECASE
  53. )
  54. if not uuid_pattern.match(user_id):
  55. return "", "用户ID格式无效"
  56. return user_id, None
  57. class RateLimiter:
  58. """简单的内存速率限制器"""
  59. def __init__(self, max_requests: int, window_seconds: int):
  60. self.max_requests = max_requests
  61. self.window_seconds = window_seconds
  62. self.requests: Dict[str, List[float]] = {}
  63. def is_allowed(self, identifier: str) -> bool:
  64. """
  65. 检查请求是否被允许
  66. Args:
  67. identifier: 请求标识符(如IP地址或用户ID)
  68. Returns:
  69. 是否允许请求
  70. """
  71. now = time.time()
  72. # 清理过期的请求记录
  73. if identifier in self.requests:
  74. self.requests[identifier] = [
  75. req_time for req_time in self.requests[identifier]
  76. if now - req_time < self.window_seconds
  77. ]
  78. else:
  79. self.requests[identifier] = []
  80. # 检查是否超过限制
  81. if len(self.requests[identifier]) >= self.max_requests:
  82. return False
  83. # 记录新请求
  84. self.requests[identifier].append(now)
  85. return True
  86. def get_remaining(self, identifier: str) -> int:
  87. """获取剩余请求次数"""
  88. now = time.time()
  89. if identifier in self.requests:
  90. valid_requests = [
  91. req_time for req_time in self.requests[identifier]
  92. if now - req_time < self.window_seconds
  93. ]
  94. return max(0, self.max_requests - len(valid_requests))
  95. return self.max_requests
  96. def rate_limit(max_requests: int, window_seconds: int, identifier_func=None):
  97. """
  98. 速率限制装饰器
  99. Args:
  100. max_requests: 最大请求数
  101. window_seconds: 时间窗口(秒)
  102. identifier_func: 获取标识符的函数,默认为IP地址
  103. """
  104. limiter = RateLimiter(max_requests, window_seconds)
  105. def decorator(f):
  106. @wraps(f)
  107. def decorated_function(*args, **kwargs):
  108. # 获取请求标识符
  109. if identifier_func:
  110. identifier = identifier_func()
  111. else:
  112. identifier = request.remote_addr or 'unknown'
  113. if not limiter.is_allowed(identifier):
  114. remaining_time = window_seconds
  115. return jsonify({
  116. 'success': False,
  117. 'error': f'请求过于频繁,请等待{remaining_time}秒后重试',
  118. 'rate_limit': {
  119. 'max_requests': max_requests,
  120. 'window_seconds': window_seconds,
  121. 'remaining': 0
  122. }
  123. }), HTTPStatus.TOO_MANY_REQUESTS
  124. # 添加速率限制信息到响应头
  125. response = f(*args, **kwargs)
  126. if hasattr(response, 'headers'):
  127. response.headers['X-RateLimit-Limit'] = str(max_requests)
  128. response.headers['X-RateLimit-Remaining'] = str(limiter.get_remaining(identifier))
  129. response.headers['X-RateLimit-Reset'] = str(int(time.time() + window_seconds))
  130. return response
  131. return decorated_function
  132. return decorator
  133. def truncate_conversation_history(history: List[Dict[str, str]], max_length: int) -> List[Dict[str, str]]:
  134. """
  135. 截断对话历史,保持最新的消息
  136. Args:
  137. history: 对话历史列表
  138. max_length: 最大历史长度
  139. Returns:
  140. 截断后的对话历史
  141. """
  142. if len(history) <= max_length:
  143. return history
  144. # 保留最新的消息,但确保至少保留用户和助手的各一条消息
  145. if max_length >= 2:
  146. return history[-max_length:]
  147. else:
  148. return history[-1:] if history else []
  149. def generate_session_id(user_id: int, conversation_id: int = None) -> str:
  150. """
  151. 生成基于时间和用户ID的session_id
  152. Args:
  153. user_id: 用户ID
  154. conversation_id: 对话ID(可选)
  155. Returns:
  156. 生成的session_id字符串(16位)
  157. """
  158. # 获取当前时间戳(毫秒)
  159. timestamp = int(time.time() * 1000)
  160. # 构建基础字符串
  161. base_string = f"{user_id}_{timestamp}"
  162. if conversation_id:
  163. base_string += f"_{conversation_id}"
  164. # 使用MD5生成哈希值,然后取前16位
  165. hash_object = hashlib.md5(base_string.encode())
  166. session_id = hash_object.hexdigest()[:16] # 只取前16位
  167. return session_id
  168. def log_request_info(message: str, user_id: str, request_method: str = None):
  169. """记录请求信息的辅助函数"""
  170. if not request_method:
  171. request_method = request.method
  172. from app import logger
  173. logger.info(
  174. f"收到{request_method}请求: "
  175. f"消息长度={len(message)}字符, "
  176. f"user_id={user_id}, "
  177. f"IP={request.remote_addr or 'unknown'}"
  178. )