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.

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