您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

904 行
43KB

  1. import requests
  2. import json
  3. import re
  4. import plugins
  5. from bridge.reply import Reply, ReplyType
  6. from bridge.context import ContextType
  7. from channel.chat_message import ChatMessage
  8. from plugins import *
  9. from common.log import logger
  10. from common.expired_dict import ExpiredDict
  11. import os
  12. from docx import Document
  13. import markdown
  14. import fitz
  15. from openpyxl import load_workbook
  16. import csv
  17. from bs4 import BeautifulSoup
  18. from pptx import Presentation
  19. from PIL import Image
  20. import base64
  21. import html
  22. EXTENSION_TO_TYPE = {
  23. 'pdf': 'pdf',
  24. 'doc': 'docx', 'docx': 'docx',
  25. 'md': 'md',
  26. 'txt': 'txt',
  27. 'xls': 'excel', 'xlsx': 'excel',
  28. 'csv': 'csv',
  29. 'html': 'html', 'htm': 'html',
  30. 'ppt': 'ppt', 'pptx': 'ppt'
  31. }
  32. @plugins.register(
  33. name="sum4all",
  34. desire_priority=2,
  35. desc="A plugin for summarizing all things",
  36. version="0.7.10",
  37. author="fatwang2",
  38. )
  39. class sum4all(Plugin):
  40. def __init__(self):
  41. super().__init__()
  42. try:
  43. curdir = os.path.dirname(__file__)
  44. config_path = os.path.join(curdir, "config.json")
  45. if os.path.exists(config_path):
  46. with open(config_path, "r", encoding="utf-8") as f:
  47. self.config = json.load(f)
  48. else:
  49. # 使用父类的方法来加载配置
  50. self.config = super().load_config()
  51. if not self.config:
  52. raise Exception("config.json not found")
  53. # 设置事件处理函数
  54. self.handlers[Event.ON_HANDLE_CONTEXT] = self.on_handle_context
  55. self.params_cache = ExpiredDict(300)
  56. # 从配置中提取所需的设置
  57. self.keys = self.config.get("keys", {})
  58. self.url_sum = self.config.get("url_sum", {})
  59. self.search_sum = self.config.get("search_sum", {})
  60. self.file_sum = self.config.get("file_sum", {})
  61. self.image_sum = self.config.get("image_sum", {})
  62. self.note = self.config.get("note", {})
  63. self.sum4all_key = self.keys.get("sum4all_key", "")
  64. self.search1api_key = self.keys.get("search1api_key", "")
  65. self.gemini_key = self.keys.get("gemini_key", "")
  66. self.bibigpt_key = self.keys.get("bibigpt_key", "")
  67. self.outputLanguage = self.keys.get("outputLanguage", "zh-CN")
  68. self.opensum_key = self.keys.get("opensum_key", "")
  69. self.open_ai_api_key = self.keys.get("open_ai_api_key", "")
  70. self.model = self.keys.get("model", "gpt-3.5-turbo")
  71. self.open_ai_api_base = self.keys.get("open_ai_api_base", "https://api.openai.com/v1")
  72. self.xunfei_app_id = self.keys.get("xunfei_app_id", "")
  73. self.xunfei_api_key = self.keys.get("xunfei_api_key", "")
  74. self.xunfei_api_secret = self.keys.get("xunfei_api_secret", "")
  75. self.perplexity_key = self.keys.get("perplexity_key", "")
  76. self.flomo_key = self.keys.get("flomo_key", "")
  77. # 提取sum服务的配置
  78. self.url_sum_enabled = self.url_sum.get("enabled", False)
  79. self.url_sum_service = self.url_sum.get("service", "")
  80. self.url_sum_group = self.url_sum.get("group", True)
  81. self.url_sum_qa_enabled = self.url_sum.get("qa_enabled", True)
  82. self.url_sum_qa_prefix = self.url_sum.get("qa_prefix", "问")
  83. self.url_sum_prompt = self.url_sum.get("prompt", "")
  84. self.search_sum_enabled = self.search_sum.get("enabled", False)
  85. self.search_sum_service = self.search_sum.get("service", "")
  86. self.search_service = self.search_sum.get("search_service", "duckduckgo")
  87. self.search_sum_group = self.search_sum.get("group", True)
  88. self.search_sum_search_prefix = self.search_sum.get("search_prefix", "搜")
  89. self.search_sum_prompt = self.search_sum.get("prompt", "")
  90. self.file_sum_enabled = self.file_sum.get("enabled", False)
  91. self.file_sum_service = self.file_sum.get("service", "")
  92. self.max_file_size = self.file_sum.get("max_file_size", 15000)
  93. self.file_sum_group = self.file_sum.get("group", True)
  94. self.file_sum_qa_prefix = self.file_sum.get("qa_prefix", "问")
  95. self.file_sum_prompt = self.file_sum.get("prompt", "")
  96. self.image_sum_enabled = self.image_sum.get("enabled", False)
  97. self.image_sum_service = self.image_sum.get("service", "")
  98. self.image_sum_group = self.image_sum.get("group", True)
  99. self.image_sum_qa_prefix = self.image_sum.get("qa_prefix", "问")
  100. self.image_sum_prompt = self.image_sum.get("prompt", "")
  101. self.note_enabled = self.note.get("enabled", False)
  102. self.note_service = self.note.get("service", "")
  103. self.note_prefix = self.note.get("prefix", "记")
  104. # 初始化成功日志
  105. logger.info("[sum4all] inited.")
  106. except Exception as e:
  107. # 初始化失败日志
  108. logger.warn(f"sum4all init failed: {e}")
  109. def on_handle_context(self, e_context: EventContext):
  110. context = e_context["context"]
  111. if context.type not in [ContextType.TEXT, ContextType.SHARING,ContextType.FILE,ContextType.IMAGE]:
  112. return
  113. msg: ChatMessage = e_context["context"]["msg"]
  114. user_id = msg.from_user_id
  115. content = context.content
  116. isgroup = e_context["context"].get("isgroup", False)
  117. url_match = re.match('https?://(?:[-\w.]|(?:%[\da-fA-F]{2}))+', content)
  118. unsupported_urls = re.search(r'.*finder\.video\.qq\.com.*|.*support\.weixin\.qq\.com/update.*|.*support\.weixin\.qq\.com/security.*|.*mp\.weixin\.qq\.com/mp/waerrpage.*', content)
  119. # 检查输入是否以"搜索前缀词" 开头
  120. if content.startswith(self.search_sum_search_prefix) and self.search_sum_enabled:
  121. # 如果消息来自一个群聊,并且你不希望在群聊中启用搜索功能,直接返回
  122. if isgroup and not self.search_sum_group:
  123. return
  124. # Call new function to handle search operation
  125. self.call_service(content, e_context, "search")
  126. return
  127. if user_id in self.params_cache and ('last_file_content' in self.params_cache[user_id] or 'last_image_base64' in self.params_cache[user_id] or 'last_url' in self.params_cache[user_id]):
  128. # 如果存在最近一次处理的文件路径,触发文件理解函数
  129. if 'last_file_content' in self.params_cache[user_id] and content.startswith(self.file_sum_qa_prefix):
  130. logger.info('Content starts with the file_sum_qa_prefix.')
  131. # 去除关键词和紧随其后的空格
  132. new_content = content[len(self.file_sum_qa_prefix):]
  133. self.params_cache[user_id]['prompt'] = new_content
  134. logger.info('params_cache for user has been successfully updated.')
  135. self.handle_file(self.params_cache[user_id]['last_file_content'], e_context)
  136. # 如果存在最近一次处理的图片路径,触发图片理解函数
  137. elif 'last_image_base64' in self.params_cache[user_id] and content.startswith(self.image_sum_qa_prefix):
  138. logger.info('Content starts with the image_sum_qa_prefix.')
  139. # 去除关键词和紧随其后的空格
  140. new_content = content[len(self.image_sum_qa_prefix):]
  141. self.params_cache[user_id]['prompt'] = new_content
  142. logger.info('params_cache for user has been successfully updated.')
  143. self.handle_image(self.params_cache[user_id]['last_image_base64'], e_context)
  144. # 如果存在最近一次处理的URL,触发URL理解函数
  145. elif 'last_url' in self.params_cache[user_id] and content.startswith(self.url_sum_qa_prefix):
  146. logger.info('Content starts with the url_sum_qa_prefix.')
  147. # 去除关键词和紧随其后的空格
  148. new_content = content[len(self.url_sum_qa_prefix):]
  149. self.params_cache[user_id]['prompt'] = new_content
  150. logger.info('params_cache for user has been successfully updated.')
  151. self.call_service(self.params_cache[user_id]['last_url'], e_context ,"sum")
  152. elif 'last_url' in self.params_cache[user_id] and content.startswith(self.note_prefix) and self.note_enabled and not isgroup:
  153. logger.info('Content starts with the note_prefix.')
  154. new_content = content[len(self.note_prefix):]
  155. self.params_cache[user_id]['note'] = new_content
  156. logger.info('params_cache for user has been successfully updated.')
  157. self.call_service(self.params_cache[user_id]['last_url'], e_context, "note")
  158. if context.type == ContextType.FILE:
  159. if isgroup and not self.file_sum_group:
  160. # 群聊中忽略处理文件
  161. logger.info("群聊消息,文件处理功能已禁用")
  162. return
  163. logger.info("on_handle_context: 处理上下文开始")
  164. context.get("msg").prepare()
  165. file_path = context.content
  166. logger.info(f"on_handle_context: 获取到文件路径 {file_path}")
  167. # 检查是否应该进行文件总结
  168. if self.file_sum_enabled:
  169. # 更新params_cache中的last_file_content
  170. self.params_cache[user_id] = {}
  171. file_content = self.extract_content(file_path)
  172. if file_content is None:
  173. logger.info("文件内容无法提取,跳过处理")
  174. else:
  175. self.params_cache[user_id]['last_file_content'] = file_content
  176. logger.info('Updated last_file_content in params_cache for user.')
  177. self.handle_file(file_content, e_context)
  178. else:
  179. logger.info("文件总结功能已禁用,不对文件内容进行处理")
  180. # 删除文件
  181. os.remove(file_path)
  182. logger.info(f"文件 {file_path} 已删除")
  183. elif context.type == ContextType.IMAGE:
  184. if isgroup and not self.image_sum_group:
  185. # 群聊中忽略处理图片
  186. logger.info("群聊消息,图片处理功能已禁用")
  187. return
  188. logger.info("on_handle_context: 开始处理图片")
  189. context.get("msg").prepare()
  190. image_path = context.content
  191. logger.info(f"on_handle_context: 获取到图片路径 {image_path}")
  192. # 检查是否应该进行图片总结
  193. if self.image_sum_enabled:
  194. # 将图片路径转换为Base64编码的字符串
  195. base64_image = self.encode_image_to_base64(image_path)
  196. # 更新params_cache中的last_image_path
  197. self.params_cache[user_id] = {}
  198. self.params_cache[user_id]['last_image_base64'] = base64_image
  199. logger.info('Updated last_image_base64 in params_cache for user.')
  200. self.handle_image(base64_image, e_context)
  201. else:
  202. logger.info("图片总结功能已禁用,不对图片内容进行处理")
  203. # 删除文件
  204. os.remove(image_path)
  205. logger.info(f"文件 {image_path} 已删除")
  206. elif context.type == ContextType.SHARING and self.url_sum_enabled: #匹配卡片分享
  207. content = html.unescape(content)
  208. if unsupported_urls: #匹配不支持总结的卡片
  209. if isgroup: ##群聊中忽略
  210. return
  211. else: ##私聊回复不支持
  212. logger.info("[sum4all] Unsupported URL : %s", content)
  213. reply = Reply(type=ReplyType.TEXT, content="不支持总结小程序和视频号")
  214. e_context["reply"] = reply
  215. e_context.action = EventAction.BREAK_PASS
  216. else: #匹配支持总结的卡片
  217. if isgroup: #处理群聊总结
  218. if self.url_sum_group: #group_sharing = True进行总结,False则忽略。
  219. logger.info("[sum4all] Summary URL : %s", content)
  220. # 更新params_cache中的last_url
  221. self.params_cache[user_id] = {}
  222. self.params_cache[user_id]['last_url'] = content
  223. logger.info('Updated last_url in params_cache for user.')
  224. self.call_service(content, e_context, "sum")
  225. return
  226. else:
  227. return
  228. else: #处理私聊总结
  229. logger.info("[sum4all] Summary URL : %s", content)
  230. # 更新params_cache中的last_url
  231. self.params_cache[user_id] = {}
  232. self.params_cache[user_id]['last_url'] = content
  233. logger.info('Updated last_url in params_cache for user.')
  234. self.call_service(content, e_context, "sum")
  235. return
  236. elif url_match and self.url_sum_enabled: #匹配URL链接
  237. if unsupported_urls: #匹配不支持总结的网址
  238. logger.info("[sum4all] Unsupported URL : %s", content)
  239. reply = Reply(type=ReplyType.TEXT, content="不支持总结小程序和视频号")
  240. e_context["reply"] = reply
  241. e_context.action = EventAction.BREAK_PASS
  242. else:
  243. logger.info("[sum4all] Summary URL : %s", content)
  244. # 更新params_cache中的last_url
  245. self.params_cache[user_id] = {}
  246. self.params_cache[user_id]['last_url'] = content
  247. logger.info('Updated last_url in params_cache for user.')
  248. self.call_service(content, e_context, "sum")
  249. return
  250. def call_service(self, content, e_context, service_type):
  251. if service_type == "search":
  252. if self.search_sum_service == "openai" or self.search_sum_service == "sum4all" or self.search_sum_service == "gemini":
  253. self.handle_search(content, e_context)
  254. elif self.search_sum_service == "perplexity":
  255. self.handle_perplexity(content, e_context)
  256. elif service_type == "sum":
  257. if self.url_sum_service == "bibigpt":
  258. self.handle_bibigpt(content, e_context)
  259. elif self.url_sum_service == "openai" or self.url_sum_service == "sum4all" or self.url_sum_service == "gemini":
  260. self.handle_url(content, e_context)
  261. elif self.url_sum_service == "opensum":
  262. self.handle_opensum(content, e_context)
  263. elif service_type == "note":
  264. if self.note_service == "flomo":
  265. self.handle_note(content, e_context)
  266. def handle_note(self,link,e_context):
  267. msg: ChatMessage = e_context["context"]["msg"]
  268. user_id = msg.from_user_id
  269. title = self.params_cache[user_id].get('title', '')
  270. content = self.params_cache[user_id].get('content', '')
  271. note = self.params_cache[user_id].get('note', '')
  272. # 将这些内容按照一定的格式整合到一起
  273. note_content = f"#sum4all\n{title}\n📒笔记:{note}\n{content}\n{link}"
  274. payload = {"content": note_content}
  275. # 将这个字典转换为JSON格式
  276. payload_json = json.dumps(payload)
  277. # 创建一个POST请求
  278. url = self.flomo_key
  279. headers = {'Content-Type': 'application/json'}
  280. # 发送这个POST请求
  281. response = requests.post(url, headers=headers, data=payload_json)
  282. reply = Reply()
  283. reply.type = ReplyType.TEXT
  284. if response.status_code == 200 and response.json()['code'] == 0:
  285. reply.content = f"已发送到{self.note_service}"
  286. else:
  287. reply.content = "发送失败,错误码:" + str(response.status_code)
  288. e_context["reply"] = reply
  289. e_context.action = EventAction.BREAK_PASS
  290. def short_url(self, long_url):
  291. url = "https://short.fatwang2.com"
  292. payload = {
  293. "url": long_url
  294. }
  295. headers = {'Content-Type': "application/json"}
  296. response = requests.request("POST", url, json=payload, headers=headers)
  297. if response.status_code == 200:
  298. res_data = response.json()
  299. # 直接从返回的 JSON 中获取短链接
  300. short_url = res_data.get('shorturl', None)
  301. if short_url:
  302. return short_url
  303. return None
  304. def handle_url(self, content, e_context):
  305. logger.info('Handling Sum4All request...')
  306. # 根据sum_service的值选择API密钥和基础URL
  307. if self.url_sum_service == "openai":
  308. api_key = self.open_ai_api_key
  309. api_base = self.open_ai_api_base
  310. model = self.model
  311. elif self.url_sum_service == "sum4all":
  312. api_key = self.sum4all_key
  313. api_base = "https://pro.sum4all.site/v1"
  314. model = "sum4all"
  315. elif self.url_sum_service == "gemini":
  316. api_key = self.gemini_key
  317. model = "gemini"
  318. api_base = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent"
  319. else:
  320. logger.error(f"未知的sum_service配置: {self.url_sum_service}")
  321. return
  322. msg: ChatMessage = e_context["context"]["msg"]
  323. user_id = msg.from_user_id
  324. user_params = self.params_cache.get(user_id, {})
  325. isgroup = e_context["context"].get("isgroup", False)
  326. prompt = user_params.get('prompt', self.url_sum_prompt)
  327. headers = {
  328. 'Content-Type': 'application/json',
  329. 'Authorization': f'Bearer {api_key}'
  330. }
  331. payload = json.dumps({
  332. "link": content,
  333. "prompt": prompt,
  334. "model": model,
  335. "base": api_base
  336. })
  337. additional_content = "" # 在 try 块之前初始化 additional_content
  338. try:
  339. logger.info('Sending request to LLM...')
  340. api_url = "https://ai.sum4all.site"
  341. response = requests.post(api_url, headers=headers, data=payload)
  342. response.raise_for_status()
  343. logger.info('Received response from LLM.')
  344. response_data = response.json() # 解析响应的 JSON 数据
  345. if response_data.get("success"):
  346. content = response_data["content"].replace("\\n", "\n") # 替换 \\n 为 \n
  347. self.params_cache[user_id]['content'] = content
  348. # 新增加的部分,用于解析 meta 数据
  349. meta = response_data.get("meta", {}) # 如果没有 meta 数据,则默认为空字典
  350. title = meta.get("og:title", "") # 获取 og:title,如果没有则默认为空字符串
  351. self.params_cache[user_id]['title'] = title
  352. # 只有当 title 非空时,才加入到回复中
  353. if title:
  354. additional_content += f"{title}\n\n"
  355. reply_content = additional_content + content # 将内容加入回复
  356. else:
  357. reply_content = "Content not found or error in response"
  358. except requests.exceptions.RequestException as e:
  359. # 处理可能出现的错误
  360. logger.error(f"Error calling new combined api: {e}")
  361. reply_content = f"An error occurred"
  362. reply = Reply()
  363. reply.type = ReplyType.TEXT
  364. if not self.url_sum_qa_enabled:
  365. reply.content = remove_markdown(reply_content)
  366. elif isgroup or not self.note_enabled:
  367. # reply.content = f"{remove_markdown(reply_content)}\n\n💬5min内输入{self.url_sum_qa_prefix}+问题,可继续追问"
  368. reply.content = f"{remove_markdown(reply_content)}"
  369. elif self.note_enabled:
  370. reply.content = f"{remove_markdown(reply_content)}\n\n💬5min内输入{self.url_sum_qa_prefix}+问题,可继续追问。\n\n📒输入{self.note_prefix}+笔记,可发送当前总结&笔记到{self.note_service}"
  371. e_context["reply"] = reply
  372. e_context.action = EventAction.BREAK_PASS
  373. def handle_bibigpt(self, content, e_context):
  374. headers = {
  375. 'Content-Type': 'application/json'
  376. }
  377. payload_params = {
  378. "url": content,
  379. "includeDetail": False,
  380. "promptConfig": {
  381. "outputLanguage": self.outputLanguage
  382. }
  383. }
  384. payload = json.dumps(payload_params)
  385. try:
  386. api_url = f"https://bibigpt.co/api/open/{self.bibigpt_key}"
  387. response = requests.request("POST",api_url, headers=headers, data=payload)
  388. response.raise_for_status()
  389. data = json.loads(response.text)
  390. summary_original = data.get('summary', 'Summary not available')
  391. html_url = data.get('htmlUrl', 'HTML URL not available')
  392. # 获取短链接
  393. short_url = self.short_url(html_url)
  394. # 如果获取短链接失败,使用 html_url
  395. if short_url is None:
  396. short_url = html_url if html_url != 'HTML URL not available' else 'URL not available'
  397. # 移除 "##摘要"、"## 亮点" 和 "-"
  398. summary = summary_original.split("详细版(支持对话追问)")[0].replace("## 摘要\n", "📌总结:").replace("## 亮点\n", "").replace("- ", "")
  399. except requests.exceptions.RequestException as e:
  400. reply = f"An error occurred"
  401. reply = Reply()
  402. reply.type = ReplyType.TEXT
  403. reply.content = f"{summary}\n\n详细链接:{short_url}"
  404. e_context["reply"] = reply
  405. e_context.action = EventAction.BREAK_PASS
  406. def handle_opensum(self, content, e_context):
  407. headers = {
  408. 'Content-Type': 'application/json',
  409. 'Authorization': f'Bearer {self.opensum_key}'
  410. }
  411. payload = json.dumps({"link": content})
  412. try:
  413. api_url = "https://read.thinkwx.com/api/v1/article/summary"
  414. response = requests.request("POST",api_url, headers=headers, data=payload)
  415. response.raise_for_status()
  416. data = json.loads(response.text)
  417. summary_data = data.get('data', {}) # 获取data字段
  418. summary_original = summary_data.get('summary', 'Summary not available')
  419. # 使用正则表达式提取URL
  420. url_pattern = r'https:\/\/[^\s]+'
  421. match = re.search(url_pattern, summary_original)
  422. html_url = match.group(0) if match else 'HTML URL not available'
  423. # 获取短链接
  424. short_url = self.short_url(html_url) if match else html_url
  425. # 用于移除摘要中的URL及其后的所有内容
  426. url_pattern_remove = r'https:\/\/[^\s]+[\s\S]*'
  427. summary = re.sub(url_pattern_remove, '', summary_original).strip()
  428. except requests.exceptions.RequestException as e:
  429. summary = f"An error occurred"
  430. short_url = 'URL not available'
  431. reply = Reply()
  432. reply.type = ReplyType.TEXT
  433. reply.content = f"{summary}\n\n详细链接:{short_url}"
  434. e_context["reply"] = reply
  435. e_context.action = EventAction.BREAK_PASS
  436. def handle_search(self, content, e_context):
  437. # 根据sum_service的值选择API密钥和基础URL
  438. if self.search_sum_service == "openai":
  439. api_key = self.open_ai_api_key
  440. api_base = self.open_ai_api_base
  441. model = self.model
  442. elif self.search_sum_service == "sum4all":
  443. api_key = self.sum4all_key
  444. api_base = "https://pro.sum4all.site/v1"
  445. model = "sum4all"
  446. elif self.search_sum_service == "gemini":
  447. api_key = self.gemini_key
  448. model = "gemini"
  449. api_base = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent"
  450. else:
  451. logger.error(f"未知的search_service配置: {self.search_sum_service}")
  452. return
  453. headers = {
  454. 'Content-Type': 'application/json',
  455. 'Authorization': f'Bearer {api_key}'
  456. }
  457. content = content[len(self.search_sum_search_prefix):]
  458. payload = json.dumps({
  459. "ur": content,
  460. "prompt": self.search_sum_prompt,
  461. "model": model,
  462. "base": api_base,
  463. "search1api_key": self.search1api_key,
  464. "search_service": self.search_service
  465. })
  466. try:
  467. api_url = "https://ai.sum4all.site"
  468. response = requests.post(api_url, headers=headers, data=payload)
  469. response.raise_for_status()
  470. response_data = response.json() # 解析响应的 JSON 数据
  471. if response_data.get("success"):
  472. content = response_data["content"].replace("\\n", "\n") # 替换 \\n 为 \n
  473. reply_content = content # 将内容加入回复
  474. # 解析 meta 数据
  475. meta = response_data.get("meta", {}) # 如果没有 meta 数据,则默认为空字典
  476. title = meta.get("og:title", "") # 获取 og:title,如果没有则默认为空字符串
  477. og_url = meta.get("og:url", "") # 获取 og:url,如果没有则默认为空字符串
  478. # 打印 title 和 og_url 以调试
  479. print("Title:", title)
  480. print("Original URL:", og_url)
  481. # 只有当 title 和 url 非空时,才加入到回复中
  482. if title:
  483. reply_content += f"\n\n参考文章:{title}"
  484. if og_url:
  485. short_url = self.short_url(og_url) # 获取短链接
  486. reply_content += f"\n\n参考链接:{short_url}"
  487. else:
  488. content = "Content not found or error in response"
  489. except requests.exceptions.RequestException as e:
  490. # 处理可能出现的错误
  491. logger.error(f"Error calling new combined api: {e}")
  492. reply_content = f"An error occurred"
  493. reply = Reply()
  494. reply.type = ReplyType.TEXT
  495. reply.content = f"{remove_markdown(reply_content)}"
  496. e_context["reply"] = reply
  497. e_context.action = EventAction.BREAK_PASS
  498. def handle_perplexity(self, content, e_context):
  499. headers = {
  500. 'Content-Type': 'application/json',
  501. 'Authorization': f'Bearer {self.perplexity_key}'
  502. }
  503. data = {
  504. "model": "sonar-small-online",
  505. "messages": [
  506. {"role": "system", "content": self.search_sum_prompt},
  507. {"role": "user", "content": content}
  508. ]
  509. }
  510. try:
  511. api_url = "https://api.perplexity.ai/chat/completions"
  512. response = requests.post(api_url, headers=headers, json=data)
  513. response.raise_for_status()
  514. # 处理响应数据
  515. response_data = response.json()
  516. # 这里可以根据你的需要处理响应数据
  517. # 解析 JSON 并获取 content
  518. if "choices" in response_data and len(response_data["choices"]) > 0:
  519. first_choice = response_data["choices"][0]
  520. if "message" in first_choice and "content" in first_choice["message"]:
  521. content = first_choice["message"]["content"]
  522. else:
  523. print("Content not found in the response")
  524. else:
  525. print("No choices available in the response")
  526. except requests.exceptions.RequestException as e:
  527. # 处理可能出现的错误
  528. logger.error(f"Error calling perplexity: {e}")
  529. reply = Reply()
  530. reply.type = ReplyType.TEXT
  531. reply.content = f"{remove_markdown(content)}"
  532. e_context["reply"] = reply
  533. e_context.action = EventAction.BREAK_PASS
  534. def get_help_text(self, verbose=False, **kwargs):
  535. help_text = "Help you summarize all things\n"
  536. if not verbose:
  537. return help_text
  538. help_text += "1.Share me the link and I will summarize it for you\n"
  539. help_text += f"2.{self.search_sum_search_prefix}+query,I will search online for you\n"
  540. return help_text
  541. def handle_file(self, content, e_context):
  542. logger.info("handle_file: 向LLM发送内容总结请求")
  543. # 根据sum_service的值选择API密钥和基础URL
  544. if self.file_sum_service == "openai":
  545. api_key = self.open_ai_api_key
  546. api_base = self.open_ai_api_base
  547. model = self.model
  548. elif self.file_sum_service == "sum4all":
  549. api_key = self.sum4all_key
  550. api_base = "https://pro.sum4all.site/v1"
  551. model = "sum4all"
  552. elif self.file_sum_service == "gemini":
  553. api_key = self.gemini_key
  554. model = "gemini"
  555. api_base = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent"
  556. else:
  557. logger.error(f"未知的sum_service配置: {self.file_sum_service}")
  558. return
  559. msg: ChatMessage = e_context["context"]["msg"]
  560. user_id = msg.from_user_id
  561. user_params = self.params_cache.get(user_id, {})
  562. prompt = user_params.get('prompt', self.file_sum_prompt)
  563. if model == "gemini":
  564. headers = {
  565. 'Content-Type': 'application/json',
  566. 'x-goog-api-key': api_key
  567. }
  568. data = {
  569. "contents": [
  570. {"role": "user", "parts": [{"text": prompt}]},
  571. {"role": "model", "parts": [{"text": "okay"}]},
  572. {"role": "user", "parts": [{"text": content}]}
  573. ],
  574. "generationConfig": {
  575. "maxOutputTokens": 800
  576. }
  577. }
  578. api_url = api_base
  579. else:
  580. headers = {
  581. 'Content-Type': 'application/json',
  582. 'Authorization': f'Bearer {api_key}'
  583. }
  584. data = {
  585. "model": model,
  586. "messages": [
  587. {"role": "system", "content": prompt},
  588. {"role": "user", "content": content}
  589. ]
  590. }
  591. api_url = f"{api_base}/chat/completions"
  592. try:
  593. response = requests.post(api_url, headers=headers, data=json.dumps(data))
  594. response.raise_for_status()
  595. response_data = response.json()
  596. # 解析 JSON 并获取 content
  597. if model == "gemini":
  598. if "candidates" in response_data and len(response_data["candidates"]) > 0:
  599. first_candidate = response_data["candidates"][0]
  600. if "content" in first_candidate:
  601. if "parts" in first_candidate["content"] and len(first_candidate["content"]["parts"]) > 0:
  602. response_content = first_candidate["content"]["parts"][0]["text"].strip() # 获取响应内容
  603. logger.info(f"Gemini API response content: {response_content}") # 记录响应内容
  604. reply_content = response_content.replace("\\n", "\n") # 替换 \\n 为 \n
  605. else:
  606. logger.error("Parts not found in the Gemini API response content")
  607. reply_content = "Parts not found in the Gemini API response content"
  608. else:
  609. logger.error("Content not found in the Gemini API response candidate")
  610. reply_content = "Content not found in the Gemini API response candidate"
  611. else:
  612. logger.error("No candidates available in the Gemini API response")
  613. reply_content = "No candidates available in the Gemini API response"
  614. else:
  615. if "choices" in response_data and len(response_data["choices"]) > 0:
  616. first_choice = response_data["choices"][0]
  617. if "message" in first_choice and "content" in first_choice["message"]:
  618. response_content = first_choice["message"]["content"].strip() # 获取响应内容
  619. logger.info(f"LLM API response content") # 记录响应内容
  620. reply_content = response_content.replace("\\n", "\n") # 替换 \\n 为 \n
  621. else:
  622. logger.error("Content not found in the response")
  623. reply_content = "Content not found in the LLM API response"
  624. else:
  625. logger.error("No choices available in the response")
  626. reply_content = "No choices available in the LLM API response"
  627. except requests.exceptions.RequestException as e:
  628. logger.error(f"Error calling LLM API: {e}")
  629. reply_content = f"An error occurred while calling LLM API"
  630. reply = Reply()
  631. reply.type = ReplyType.TEXT
  632. # reply.content = f"{remove_markdown(reply_content)}\n\n💬5min内输入{self.file_sum_qa_prefix}+问题,可继续追问"
  633. reply.content = f"{remove_markdown(reply_content)}"
  634. e_context["reply"] = reply
  635. e_context.action = EventAction.BREAK_PASS
  636. def read_pdf(self, file_path):
  637. logger.info(f"开始读取PDF文件:{file_path}")
  638. doc = fitz.open(file_path)
  639. content = ' '.join([page.get_text() for page in doc])
  640. logger.info(f"PDF文件读取完成:{file_path}")
  641. return content
  642. def read_word(self, file_path):
  643. doc = Document(file_path)
  644. return ' '.join([p.text for p in doc.paragraphs])
  645. def read_markdown(self, file_path):
  646. with open(file_path, 'r', encoding='utf-8') as file:
  647. md_content = file.read()
  648. return markdown.markdown(md_content)
  649. def read_excel(self, file_path):
  650. workbook = load_workbook(file_path)
  651. content = ''
  652. for sheet in workbook:
  653. for row in sheet.iter_rows():
  654. content += ' '.join([str(cell.value) for cell in row])
  655. content += '\n'
  656. return content
  657. def read_txt(self, file_path):
  658. logger.debug(f"开始读取TXT文件: {file_path}")
  659. try:
  660. with open(file_path, 'r', encoding='utf-8') as file:
  661. content = file.read()
  662. logger.debug(f"TXT文件读取完成: {file_path}")
  663. logger.debug("TXT文件内容的前50个字符:")
  664. logger.debug(content[:50]) # 打印文件内容的前50个字符
  665. return content
  666. except Exception as e:
  667. logger.error(f"读取TXT文件时出错: {file_path},错误信息: {str(e)}")
  668. return ""
  669. def read_csv(self, file_path):
  670. content = ''
  671. with open(file_path, 'r', encoding='utf-8') as csvfile:
  672. reader = csv.reader(csvfile)
  673. for row in reader:
  674. content += ' '.join(row) + '\n'
  675. return content
  676. def read_html(self, file_path):
  677. with open(file_path, 'r', encoding='utf-8') as file:
  678. soup = BeautifulSoup(file, 'html.parser')
  679. return soup.get_text()
  680. def read_ppt(self, file_path):
  681. presentation = Presentation(file_path)
  682. content = ''
  683. for slide in presentation.slides:
  684. for shape in slide.shapes:
  685. if hasattr(shape, "text"):
  686. content += shape.text + '\n'
  687. return content
  688. def extract_content(self, file_path):
  689. logger.info(f"extract_content: 提取文件内容,文件路径: {file_path}")
  690. file_size = os.path.getsize(file_path) // 1000 # 将文件大小转换为KB
  691. if file_size > int(self.max_file_size):
  692. logger.warning(f"文件大小超过限制({self.max_file_size}KB),不进行处理。文件大小: {file_size}KB")
  693. return None
  694. file_extension = os.path.splitext(file_path)[1][1:].lower()
  695. logger.info(f"extract_content: 文件类型为 {file_extension}")
  696. file_type = EXTENSION_TO_TYPE.get(file_extension)
  697. if not file_type:
  698. logger.error(f"不支持的文件扩展名: {file_extension}")
  699. return None
  700. read_func = {
  701. 'pdf': self.read_pdf,
  702. 'docx': self.read_word,
  703. 'md': self.read_markdown,
  704. 'txt': self.read_txt,
  705. 'excel': self.read_excel,
  706. 'csv': self.read_csv,
  707. 'html': self.read_html,
  708. 'ppt': self.read_ppt
  709. }.get(file_type)
  710. if not read_func:
  711. logger.error(f"不支持的文件类型: {file_type}")
  712. return None
  713. logger.info("extract_content: 文件内容提取完成")
  714. return read_func(file_path)
  715. def encode_image_to_base64(self, image_path):
  716. logger.info(f"开始处理图片: {image_path}")
  717. try:
  718. with Image.open(image_path) as img:
  719. logger.info(f"成功打开图片. 原始大小: {img.size}")
  720. if img.width > 1024:
  721. new_size = (1024, int(img.height*1024/img.width))
  722. img = img.resize(new_size)
  723. img.save(image_path) # 保存调整大小后的图片
  724. logger.info(f"调整图片大小至: {new_size}")
  725. with open(image_path, "rb") as image_file:
  726. img_byte_arr = image_file.read()
  727. logger.info(f"读取图片完成. 大小: {len(img_byte_arr)} 字节")
  728. encoded = base64.b64encode(img_byte_arr).decode('ascii')
  729. logger.info(f"Base64编码完成. 编码后长度: {len(encoded)}")
  730. return encoded
  731. except Exception as e:
  732. logger.error(f"图片编码过程中发生错误: {str(e)}", exc_info=True)
  733. raise
  734. # Function to handle OpenAI image processing
  735. def handle_image(self, base64_image, e_context):
  736. logger.info("handle_image: 解析图像处理API的响应")
  737. msg: ChatMessage = e_context["context"]["msg"]
  738. user_id = msg.from_user_id
  739. user_params = self.params_cache.get(user_id, {})
  740. prompt = user_params.get('prompt', self.image_sum_prompt)
  741. if self.image_sum_service == "openai":
  742. api_key = self.open_ai_api_key
  743. api_base = f"{self.open_ai_api_base}/chat/completions"
  744. model = "gpt-4o-mini"
  745. elif self.image_sum_service == "xunfei":
  746. api_key = self.xunfei_api_key
  747. api_base = "https://spark.sum4all.site/v1/chat/completions"
  748. model = "spark-chat-vision"
  749. elif self.image_sum_service == "sum4all":
  750. api_key = self.sum4all_key
  751. api_base = "https://pro.sum4all.site/v1/chat/completions"
  752. model = "sum4all-vision"
  753. elif self.image_sum_service == "gemini":
  754. api_key = self.gemini_key
  755. api_base = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent"
  756. payload = {
  757. "contents": [
  758. {
  759. "parts": [
  760. {"text": prompt},
  761. {
  762. "inline_data": {
  763. "mime_type":"image/png",
  764. "data": base64_image
  765. }
  766. }
  767. ]
  768. }
  769. ]
  770. }
  771. headers = {
  772. "Content-Type": "application/json",
  773. "x-goog-api-key": api_key
  774. }
  775. logger.info(f"准备发送请求. Payload大小: {len(json.dumps(payload))} 字节")
  776. else:
  777. logger.error(f"未知的image_sum_service配置: {self.image_sum_service}")
  778. return
  779. if self.image_sum_service != "gemini":
  780. payload = {
  781. "model": model,
  782. "messages": [
  783. {
  784. "role": "user",
  785. "content": [
  786. {
  787. "type": "text",
  788. "text": prompt
  789. },
  790. {
  791. "type": "image_url",
  792. "image_url": {
  793. "url": f"data:image/jpeg;base64,{base64_image}"
  794. }
  795. }
  796. ]
  797. }
  798. ],
  799. "max_tokens": 3000
  800. }
  801. headers = {
  802. "Content-Type": "application/json",
  803. "Authorization": f"Bearer {api_key}"
  804. }
  805. try:
  806. response = requests.post(api_base, headers=headers, json=payload)
  807. logger.info(f"API请求已发送. 状态码: {response.status_code}")
  808. response.raise_for_status()
  809. logger.info("API响应状态码正常,开始解析JSON")
  810. response_json = response.json()
  811. logger.info("JSON解析完成")
  812. if self.image_sum_service == "gemini":
  813. reply_content = response_json.get('candidates', [{}])[0].get('content', {}).get('parts', [{}])[0].get('text', 'No text found in the response')
  814. logger.info(f"成功解析Gemini响应. 回复内容长度: {len(reply_content)}")
  815. else:
  816. if "choices" in response_json and len(response_json["choices"]) > 0:
  817. first_choice = response_json["choices"][0]
  818. if "message" in first_choice and "content" in first_choice["message"]:
  819. response_content = first_choice["message"]["content"].strip()
  820. logger.info("LLM API response content")
  821. reply_content = response_content
  822. else:
  823. logger.error("Content not found in the response")
  824. reply_content = "Content not found in the LLM API response"
  825. else:
  826. logger.error("No choices available in the response")
  827. reply_content = "No choices available in the LLM API response"
  828. except Exception as e:
  829. logger.error(f"Error processing LLM API response: {e}")
  830. reply_content = f"An error occurred while processing LLM API response"
  831. reply = Reply()
  832. reply.type = ReplyType.TEXT
  833. # reply.content = f"{remove_markdown(reply_content)}\n\n💬5min内输入{self.image_sum_qa_prefix}+问题,可继续追问"
  834. reply.content = f"{remove_markdown(reply_content)}"
  835. e_context["reply"] = reply
  836. e_context.action = EventAction.BREAK_PASS
  837. def remove_markdown(text):
  838. # 替换Markdown的粗体标记
  839. text = text.replace("**", "")
  840. # 替换Markdown的标题标记
  841. text = text.replace("### ", "").replace("## ", "").replace("# ", "")
  842. return text