Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

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