You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

261 lines
8.4KB

  1. import io
  2. import os
  3. import uuid
  4. import requests
  5. from urllib.parse import urlparse
  6. from PIL import Image
  7. from common.log import logger
  8. import oss2,time,json
  9. from urllib.parse import urlparse, unquote
  10. from common import redis_helper
  11. from datetime import datetime
  12. def fsize(file):
  13. if isinstance(file, io.BytesIO):
  14. return file.getbuffer().nbytes
  15. elif isinstance(file, str):
  16. return os.path.getsize(file)
  17. elif hasattr(file, "seek") and hasattr(file, "tell"):
  18. pos = file.tell()
  19. file.seek(0, os.SEEK_END)
  20. size = file.tell()
  21. file.seek(pos)
  22. return size
  23. else:
  24. raise TypeError("Unsupported type")
  25. def compress_imgfile(file, max_size):
  26. if fsize(file) <= max_size:
  27. return file
  28. file.seek(0)
  29. img = Image.open(file)
  30. rgb_image = img.convert("RGB")
  31. quality = 95
  32. while True:
  33. out_buf = io.BytesIO()
  34. rgb_image.save(out_buf, "JPEG", quality=quality)
  35. if fsize(out_buf) <= max_size:
  36. return out_buf
  37. quality -= 5
  38. def split_string_by_utf8_length(string, max_length, max_split=0):
  39. encoded = string.encode("utf-8")
  40. start, end = 0, 0
  41. result = []
  42. while end < len(encoded):
  43. if max_split > 0 and len(result) >= max_split:
  44. result.append(encoded[start:].decode("utf-8"))
  45. break
  46. end = min(start + max_length, len(encoded))
  47. # 如果当前字节不是 UTF-8 编码的开始字节,则向前查找直到找到开始字节为止
  48. while end < len(encoded) and (encoded[end] & 0b11000000) == 0b10000000:
  49. end -= 1
  50. result.append(encoded[start:end].decode("utf-8"))
  51. start = end
  52. return result
  53. def get_path_suffix(path):
  54. path = urlparse(path).path
  55. return os.path.splitext(path)[-1].lstrip('.')
  56. def convert_webp_to_png(webp_image):
  57. from PIL import Image
  58. try:
  59. webp_image.seek(0)
  60. img = Image.open(webp_image).convert("RGBA")
  61. png_image = io.BytesIO()
  62. img.save(png_image, format="PNG")
  63. png_image.seek(0)
  64. return png_image
  65. except Exception as e:
  66. logger.error(f"Failed to convert WEBP to PNG: {e}")
  67. raise
  68. def generate_timestamp():
  69. # 获取当前时间
  70. now = datetime.now()
  71. # 格式化时间字符串为 'yyyyMMddHHmmssSS'
  72. timestamp = now.strftime('%Y%m%d%H%M%S%f')[:-4]
  73. return timestamp
  74. def at_extract_content(text):
  75. # 找到最后一个空格的索引
  76. last_space_index = text.rfind(" ")
  77. if last_space_index != -1:
  78. # 返回空格后面的内容
  79. return text[last_space_index + 1:]
  80. return ""
  81. def audio_extract_content(text):
  82. result = text.split('\n', 1)[1]
  83. return result
  84. def save_to_local_from_url(url):
  85. '''
  86. 从url保存到本地tmp目录
  87. '''
  88. parsed_url = urlparse(url)
  89. # 从 URL 提取文件名
  90. filename = os.path.basename(parsed_url.path)
  91. # tmp_dir = os.path(__file__) # 获取系统临时目录
  92. # print(tmp_dir)
  93. tmp_file_path = os.path.join(os.getcwd(),'tmp', filename) # 拼接完整路径
  94. # 检查是否存在同名文件
  95. if os.path.exists(tmp_file_path):
  96. logger.info(f"文件已存在,将覆盖:{tmp_file_path}")
  97. # 下载文件并保存到临时目录
  98. response = requests.get(url, stream=True)
  99. with open(tmp_file_path, 'wb') as f:
  100. for chunk in response.iter_content(chunk_size=1024):
  101. if chunk: # 检查是否有内容
  102. f.write(chunk)
  103. return tmp_file_path
  104. def upload_oss(
  105. access_key_id,
  106. access_key_secret,
  107. endpoint,
  108. bucket_name,
  109. file_source,
  110. prefix,
  111. expiration_days=7
  112. ):
  113. """
  114. 上传文件到阿里云OSS并设置生命周期规则,同时返回文件的公共访问地址。
  115. :param access_key_id: 阿里云AccessKey ID
  116. :param access_key_secret: 阿里云AccessKey Secret
  117. :param endpoint: OSS区域对应的Endpoint
  118. :param bucket_name: OSS中的Bucket名称
  119. :param file_source: 本地文件路径或HTTP链接
  120. :param prefix: 设置规则应用的前缀为文件所在目录
  121. :param expiration_days: 文件保存天数,默认7天后删除
  122. :return: 文件的公共访问地址
  123. """
  124. # 创建Bucket实例
  125. auth = oss2.Auth(access_key_id, access_key_secret)
  126. bucket = oss2.Bucket(auth, endpoint, bucket_name)
  127. ### 1. 设置生命周期规则 ###
  128. rule_id = f'delete_after_{expiration_days}_days' # 规则ID
  129. # prefix = oss_file_name.split('/')[0] + '/' # 设置规则应用的前缀为文件所在目录
  130. # 定义生命周期规则
  131. rule = oss2.models.LifecycleRule(rule_id, prefix, status=oss2.models.LifecycleRule.ENABLED,
  132. expiration=oss2.models.LifecycleExpiration(days=expiration_days))
  133. # 设置Bucket的生命周期
  134. lifecycle = oss2.models.BucketLifecycle([rule])
  135. bucket.put_bucket_lifecycle(lifecycle)
  136. print(f"已设置生命周期规则:文件将在{expiration_days}天后自动删除")
  137. ### 2. 判断文件来源并上传到OSS ###
  138. if file_source.startswith('http://') or file_source.startswith('https://'):
  139. # HTTP 链接,先下载文件
  140. try:
  141. response = requests.get(file_source, stream=True)
  142. response.raise_for_status()
  143. parsed_url = urlparse(file_source)
  144. # 提取路径部分并解码
  145. path = unquote(parsed_url.path)
  146. # 获取路径的最后一部分作为文件名
  147. filename = path.split('/')[-1]
  148. oss_file_name=prefix+'/'+ filename
  149. bucket.put_object(oss_file_name, response.content)
  150. print(f"文件从 HTTP 链接上传成功:{file_source}")
  151. except requests.exceptions.RequestException as e:
  152. print(f"从 HTTP 链接下载文件失败: {e}")
  153. return None
  154. else:
  155. # 本地文件路径
  156. try:
  157. filename=os.path.basename(file_source)
  158. oss_file_name=prefix+'/'+ filename
  159. bucket.put_object_from_file(oss_file_name, file_source)
  160. print(f"文件从本地路径上传成功:{file_source}")
  161. except oss2.exceptions.OssError as e:
  162. print(f"从本地路径上传文件失败: {e}")
  163. return None
  164. ### 3. 构建公共访问URL ###
  165. file_url = f"http://{bucket_name}.{endpoint.replace('http://', '')}/{oss_file_name}"
  166. print(f"文件上传成功,公共访问地址:{file_url}")
  167. return file_url
  168. def generate_guid_no_dashes():
  169. """
  170. 生成一个无分隔符的 GUID
  171. :return: 返回生成的无分隔符 GUID 字符串
  172. """
  173. return str(uuid.uuid4()).replace('-', '')
  174. def dialogue_message(wxid_from:str,wxid_to:str,wx_content:list,is_ai:bool=False):
  175. """
  176. 构造消息的 JSON 数据
  177. :param contents: list,包含多个消息内容,每个内容为字典,如:
  178. [{"type": "text", "text": "AAAAAAA"},
  179. {"type": "image_url", "image_url": {"url": "https://AAAAA.jpg"}},
  180. {"type":"file","file_url":{"url":"https://AAAAA.pdf"}}
  181. ]
  182. :return: JSON 字符串
  183. """
  184. # 获取当前时间戳,精确到毫秒
  185. current_timestamp = int(time.time() * 1000)
  186. # 获取当前时间,格式化为 "YYYY-MM-DD HH:MM:SS"
  187. current_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
  188. # 构造 JSON 数据
  189. data = {
  190. "message_id": str(current_timestamp),
  191. "topic": "topic.ai.ops.wx",
  192. "time": current_time,
  193. "data": {
  194. "msg_type": "dialogue",
  195. "is_ai":is_ai,
  196. "content": {
  197. "wxid_from": wxid_from,
  198. "wxid_to": wxid_to,
  199. "wx_content":wx_content
  200. }
  201. }
  202. }
  203. return json.dumps(data, separators=(',', ':'), ensure_ascii=False)
  204. def save_contacts_brief_to_redis(wxid, friends):
  205. # 将联系人信息保存到 Redis,使用一个合适的 key
  206. hash_key = f"__AI_OPS_WX__:CONTACTS_BRIEF:{wxid}"
  207. # 获取缓存中的数据,如果缓存不存在则初始化为空列表
  208. cache_str = redis_helper.redis_helper.get_hash_field(hash_key, "data")
  209. cache = json.loads(cache_str) if cache_str else []
  210. # 合并联系人信息
  211. cache.extend(friends)
  212. # 将合并后的联系人数据保存回 Redis
  213. redis_helper.redis_helper.update_hash_field(hash_key, "data", {
  214. "data": json.dumps(cache, ensure_ascii=False)
  215. })