Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

284 lines
12KB

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