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.

linkai.py 13KB

11 kuukautta sitten
11 kuukautta sitten
10 kuukautta sitten
11 kuukautta sitten
11 kuukautta sitten
11 kuukautta sitten
10 kuukautta sitten
1 vuosi sitten
11 kuukautta sitten
11 kuukautta sitten
11 kuukautta sitten
11 kuukautta sitten
11 kuukautta sitten
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. import plugins
  2. from bridge.context import ContextType
  3. from bridge.reply import Reply, ReplyType
  4. from plugins import *
  5. from .midjourney import MJBot
  6. from .summary import LinkSummary
  7. from bridge import bridge
  8. from common.expired_dict import ExpiredDict
  9. from common import const
  10. import os
  11. from .utils import Util
  12. from config import plugin_config
  13. @plugins.register(
  14. name="linkai",
  15. desc="A plugin that supports knowledge base and midjourney drawing.",
  16. version="0.1.0",
  17. author="https://link-ai.tech",
  18. desire_priority=99
  19. )
  20. class LinkAI(Plugin):
  21. def __init__(self):
  22. super().__init__()
  23. self.handlers[Event.ON_HANDLE_CONTEXT] = self.on_handle_context
  24. self.config = super().load_config()
  25. if not self.config:
  26. # 未加载到配置,使用模板中的配置
  27. self.config = self._load_config_template()
  28. if self.config:
  29. self.mj_bot = MJBot(self.config.get("midjourney"))
  30. self.sum_config = {}
  31. if self.config:
  32. self.sum_config = self.config.get("summary")
  33. logger.info(f"[LinkAI] inited, config={self.config}")
  34. def on_handle_context(self, e_context: EventContext):
  35. """
  36. 消息处理逻辑
  37. :param e_context: 消息上下文
  38. """
  39. if not self.config:
  40. return
  41. context = e_context['context']
  42. if context.type not in [ContextType.TEXT, ContextType.IMAGE, ContextType.IMAGE_CREATE, ContextType.FILE,
  43. ContextType.SHARING]:
  44. # filter content no need solve
  45. return
  46. if context.type in [ContextType.FILE, ContextType.IMAGE] and self._is_summary_open(context):
  47. # 文件处理
  48. context.get("msg").prepare()
  49. file_path = context.content
  50. if not LinkSummary().check_file(file_path, self.sum_config):
  51. return
  52. if context.type != ContextType.IMAGE:
  53. _send_info(e_context, "正在为你加速生成摘要,请稍后")
  54. res = LinkSummary().summary_file(file_path)
  55. if not res:
  56. if context.type != ContextType.IMAGE:
  57. _set_reply_text("因为神秘力量无法获取内容,请稍后再试吧", e_context, level=ReplyType.TEXT)
  58. return
  59. summary_text = res.get("summary")
  60. if context.type != ContextType.IMAGE:
  61. USER_FILE_MAP[_find_user_id(context) + "-sum_id"] = res.get("summary_id")
  62. summary_text += "\n\n💬 发送 \"开启对话\" 可以开启与文件内容的对话"
  63. _set_reply_text(summary_text, e_context, level=ReplyType.TEXT)
  64. os.remove(file_path)
  65. return
  66. if (context.type == ContextType.SHARING and self._is_summary_open(context)) or \
  67. (context.type == ContextType.TEXT and self._is_summary_open(context) and LinkSummary().check_url(context.content)):
  68. if not LinkSummary().check_url(context.content):
  69. return
  70. _send_info(e_context, "正在为你加速生成摘要,请稍后")
  71. res = LinkSummary().summary_url(context.content)
  72. if not res:
  73. _set_reply_text("因为神秘力量无法获取文章内容,请稍后再试吧~", e_context, level=ReplyType.TEXT)
  74. return
  75. _set_reply_text(res.get("summary") + "\n\n💬 发送 \"开启对话\" 可以开启与文章内容的对话", e_context,
  76. level=ReplyType.TEXT)
  77. USER_FILE_MAP[_find_user_id(context) + "-sum_id"] = res.get("summary_id")
  78. return
  79. mj_type = self.mj_bot.judge_mj_task_type(e_context)
  80. if mj_type:
  81. # MJ作图任务处理
  82. self.mj_bot.process_mj_task(mj_type, e_context)
  83. return
  84. if context.content.startswith(f"{_get_trigger_prefix()}linkai"):
  85. # 应用管理功能
  86. self._process_admin_cmd(e_context)
  87. return
  88. if context.type == ContextType.TEXT and context.content == "开启对话" and _find_sum_id(context):
  89. # 文本对话
  90. _send_info(e_context, "正在为你开启对话,请稍后")
  91. res = LinkSummary().summary_chat(_find_sum_id(context))
  92. if not res:
  93. _set_reply_text("开启对话失败,请稍后再试吧", e_context)
  94. return
  95. USER_FILE_MAP[_find_user_id(context) + "-file_id"] = res.get("file_id")
  96. _set_reply_text("💡你可以问我关于这篇文章的任何问题,例如:\n\n" + res.get(
  97. "questions") + "\n\n发送 \"退出对话\" 可以关闭与文章的对话", e_context, level=ReplyType.TEXT)
  98. return
  99. if context.type == ContextType.TEXT and context.content == "退出对话" and _find_file_id(context):
  100. del USER_FILE_MAP[_find_user_id(context) + "-file_id"]
  101. bot = bridge.Bridge().find_chat_bot(const.LINKAI)
  102. bot.sessions.clear_session(context["session_id"])
  103. _set_reply_text("对话已退出", e_context, level=ReplyType.TEXT)
  104. return
  105. if context.type == ContextType.TEXT and _find_file_id(context):
  106. bot = bridge.Bridge().find_chat_bot(const.LINKAI)
  107. context.kwargs["file_id"] = _find_file_id(context)
  108. reply = bot.reply(context.content, context)
  109. e_context["reply"] = reply
  110. e_context.action = EventAction.BREAK_PASS
  111. return
  112. if self._is_chat_task(e_context):
  113. # 文本对话任务处理
  114. self._process_chat_task(e_context)
  115. # 插件管理功能
  116. def _process_admin_cmd(self, e_context: EventContext):
  117. context = e_context['context']
  118. cmd = context.content.split()
  119. if len(cmd) == 1 or (len(cmd) == 2 and cmd[1] == "help"):
  120. _set_reply_text(self.get_help_text(verbose=True), e_context, level=ReplyType.INFO)
  121. return
  122. if len(cmd) == 2 and (cmd[1] == "open" or cmd[1] == "close"):
  123. # 知识库开关指令
  124. if not Util.is_admin(e_context):
  125. _set_reply_text("需要管理员权限执行", e_context, level=ReplyType.ERROR)
  126. return
  127. is_open = True
  128. tips_text = "开启"
  129. if cmd[1] == "close":
  130. tips_text = "关闭"
  131. is_open = False
  132. conf()["use_linkai"] = is_open
  133. bridge.Bridge().reset_bot()
  134. _set_reply_text(f"LinkAI对话功能{tips_text}", e_context, level=ReplyType.INFO)
  135. return
  136. if len(cmd) == 3 and cmd[1] == "app":
  137. # 知识库应用切换指令
  138. if not context.kwargs.get("isgroup"):
  139. _set_reply_text("该指令需在群聊中使用", e_context, level=ReplyType.ERROR)
  140. return
  141. if not Util.is_admin(e_context):
  142. _set_reply_text("需要管理员权限执行", e_context, level=ReplyType.ERROR)
  143. return
  144. app_code = cmd[2]
  145. group_name = context.kwargs.get("msg").from_user_nickname
  146. group_mapping = self.config.get("group_app_map")
  147. if group_mapping:
  148. group_mapping[group_name] = app_code
  149. else:
  150. self.config["group_app_map"] = {group_name: app_code}
  151. # 保存插件配置
  152. super().save_config(self.config)
  153. _set_reply_text(f"应用设置成功: {app_code}", e_context, level=ReplyType.INFO)
  154. return
  155. if len(cmd) == 3 and cmd[1] == "sum" and (cmd[2] == "open" or cmd[2] == "close"):
  156. # 知识库开关指令
  157. if not Util.is_admin(e_context):
  158. _set_reply_text("需要管理员权限执行", e_context, level=ReplyType.ERROR)
  159. return
  160. is_open = True
  161. tips_text = "开启"
  162. if cmd[2] == "close":
  163. tips_text = "关闭"
  164. is_open = False
  165. if not self.sum_config:
  166. _set_reply_text(
  167. f"插件未启用summary功能,请参考以下链添加插件配置\n\nhttps://github.com/zhayujie/chatgpt-on-wechat/blob/master/plugins/linkai/README.md",
  168. e_context, level=ReplyType.INFO)
  169. else:
  170. self.sum_config["enabled"] = is_open
  171. _set_reply_text(f"文章总结功能{tips_text}", e_context, level=ReplyType.INFO)
  172. return
  173. _set_reply_text(f"指令错误,请输入{_get_trigger_prefix()}linkai help 获取帮助", e_context,
  174. level=ReplyType.INFO)
  175. return
  176. def _is_summary_open(self, context) -> bool:
  177. if not self.sum_config or not self.sum_config.get("enabled"):
  178. return False
  179. if context.kwargs.get("isgroup") and not self.sum_config.get("group_enabled"):
  180. return False
  181. support_type = self.sum_config.get("type") or ["FILE", "SHARING"]
  182. if context.type.name not in support_type and context.type.name != "TEXT":
  183. return False
  184. return True
  185. # LinkAI 对话任务处理
  186. def _is_chat_task(self, e_context: EventContext):
  187. context = e_context['context']
  188. # 群聊应用管理
  189. return self.config.get("group_app_map") and context.kwargs.get("isgroup")
  190. def _process_chat_task(self, e_context: EventContext):
  191. """
  192. 处理LinkAI对话任务
  193. :param e_context: 对话上下文
  194. """
  195. context = e_context['context']
  196. # 群聊应用管理
  197. group_name = context.get("msg").from_user_nickname
  198. app_code = self._fetch_group_app_code(group_name)
  199. if app_code:
  200. context.kwargs['app_code'] = app_code
  201. def _fetch_group_app_code(self, group_name: str) -> str:
  202. """
  203. 根据群聊名称获取对应的应用code
  204. :param group_name: 群聊名称
  205. :return: 应用code
  206. """
  207. group_mapping = self.config.get("group_app_map")
  208. if group_mapping:
  209. app_code = group_mapping.get(group_name) or group_mapping.get("ALL_GROUP")
  210. return app_code
  211. def get_help_text(self, verbose=False, **kwargs):
  212. trigger_prefix = _get_trigger_prefix()
  213. help_text = "用于集成 LinkAI 提供的知识库、Midjourney绘画、文档总结、联网搜索等能力。\n\n"
  214. if not verbose:
  215. return help_text
  216. help_text += f'📖 知识库\n - 群聊中指定应用: {trigger_prefix}linkai app 应用编码\n'
  217. help_text += f' - {trigger_prefix}linkai open: 开启对话\n'
  218. help_text += f' - {trigger_prefix}linkai close: 关闭对话\n'
  219. help_text += f'\n例如: \n"{trigger_prefix}linkai app Kv2fXJcH"\n\n'
  220. help_text += f"🎨 绘画\n - 生成: {trigger_prefix}mj 描述词1, 描述词2.. \n - 放大: {trigger_prefix}mju 图片ID 图片序号\n - 变换: {trigger_prefix}mjv 图片ID 图片序号\n - 重置: {trigger_prefix}mjr 图片ID"
  221. help_text += f"\n\n例如:\n\"{trigger_prefix}mj a little cat, white --ar 9:16\"\n\"{trigger_prefix}mju 11055927171882 2\""
  222. help_text += f"\n\"{trigger_prefix}mjv 11055927171882 2\"\n\"{trigger_prefix}mjr 11055927171882\""
  223. help_text += f"\n\n💡 文档总结和对话\n - 开启: {trigger_prefix}linkai sum open\n - 使用: 发送文件、公众号文章等可生成摘要,并与内容对话"
  224. return help_text
  225. def _load_config_template(self):
  226. logger.debug("No LinkAI plugin config.json, use plugins/linkai/config.json.template")
  227. try:
  228. plugin_config_path = os.path.join(self.path, "config.json.template")
  229. if os.path.exists(plugin_config_path):
  230. with open(plugin_config_path, "r", encoding="utf-8") as f:
  231. plugin_conf = json.load(f)
  232. plugin_conf["midjourney"]["enabled"] = False
  233. plugin_conf["summary"]["enabled"] = False
  234. plugin_config["linkai"] = plugin_conf
  235. return plugin_conf
  236. except Exception as e:
  237. logger.exception(e)
  238. def reload(self):
  239. self.config = super().load_config()
  240. def _send_info(e_context: EventContext, content: str):
  241. reply = Reply(ReplyType.TEXT, content)
  242. channel = e_context["channel"]
  243. channel.send(reply, e_context["context"])
  244. def _find_user_id(context):
  245. if context["isgroup"]:
  246. return context.kwargs.get("msg").actual_user_id
  247. else:
  248. return context["receiver"]
  249. def _set_reply_text(content: str, e_context: EventContext, level: ReplyType = ReplyType.ERROR):
  250. reply = Reply(level, content)
  251. e_context["reply"] = reply
  252. e_context.action = EventAction.BREAK_PASS
  253. def _get_trigger_prefix():
  254. return conf().get("plugin_trigger_prefix", "$")
  255. def _find_sum_id(context):
  256. return USER_FILE_MAP.get(_find_user_id(context) + "-sum_id")
  257. def _find_file_id(context):
  258. user_id = _find_user_id(context)
  259. if user_id:
  260. return USER_FILE_MAP.get(user_id + "-file_id")
  261. USER_FILE_MAP = ExpiredDict(conf().get("expires_in_seconds") or 60 * 30)