Kaynağa Gözat

Merge pull request #1357 from zhayujie/feat-1.3.5

feat: add plugin instructions and fix some issues
zhayujie 2 yıl önce
ebeveyn
işleme
176941ea3b

+ 6 - 0
bridge/bridge.py

@@ -54,3 +54,9 @@ class Bridge(object):
 
 
     def fetch_translate(self, text, from_lang="", to_lang="en") -> Reply:
     def fetch_translate(self, text, from_lang="", to_lang="en") -> Reply:
         return self.get_bot("translate").translate(text, from_lang, to_lang)
         return self.get_bot("translate").translate(text, from_lang, to_lang)
+
+    def reset_bot(self):
+        """
+        重置bot路由
+        """
+        self.__init__()

+ 6 - 2
channel/chat_channel.py

@@ -108,8 +108,12 @@ class ChatChannel(Channel):
                     if not conf().get("group_at_off", False):
                     if not conf().get("group_at_off", False):
                         flag = True
                         flag = True
                     pattern = f"@{re.escape(self.name)}(\u2005|\u0020)"
                     pattern = f"@{re.escape(self.name)}(\u2005|\u0020)"
-                    content = re.sub(pattern, r"", content)
-
+                    subtract_res = re.sub(pattern, r"", content)
+                    if subtract_res == content and context["msg"].self_display_name:
+                        # 前缀移除后没有变化,使用群昵称再次移除
+                        pattern = f"@{re.escape(context['msg'].self_display_name)}(\u2005|\u0020)"
+                        subtract_res = re.sub(pattern, r"", content)
+                    content = subtract_res
                 if not flag:
                 if not flag:
                     if context["origin_ctype"] == ContextType.VOICE:
                     if context["origin_ctype"] == ContextType.VOICE:
                         logger.info("[WX]receive group voice, but checkprefix didn't match")
                         logger.info("[WX]receive group voice, but checkprefix didn't match")

+ 2 - 3
channel/chat_message.py

@@ -24,9 +24,7 @@ is_at: 是否被at
 - (群消息时,一般会存在实际发送者,是群内某个成员的id和昵称,下列项仅在群消息时存在)
 - (群消息时,一般会存在实际发送者,是群内某个成员的id和昵称,下列项仅在群消息时存在)
 actual_user_id: 实际发送者id (群聊必填)
 actual_user_id: 实际发送者id (群聊必填)
 actual_user_nickname:实际发送者昵称
 actual_user_nickname:实际发送者昵称
-
-
-
+self_display_name: 自身的展示名,设置群昵称时,该字段表示群昵称
 
 
 _prepare_fn: 准备函数,用于准备消息的内容,比如下载图片等,
 _prepare_fn: 准备函数,用于准备消息的内容,比如下载图片等,
 _prepared: 是否已经调用过准备函数
 _prepared: 是否已经调用过准备函数
@@ -49,6 +47,7 @@ class ChatMessage(object):
     other_user_id = None
     other_user_id = None
     other_user_nickname = None
     other_user_nickname = None
     my_msg = False
     my_msg = False
+    self_display_name = None
 
 
     is_group = False
     is_group = False
     is_at = False
     is_at = False

+ 1 - 1
channel/wechat/wechat_channel.py

@@ -58,7 +58,7 @@ def _check(func):
         if conf().get("hot_reload") == True and int(create_time) < int(time.time()) - 60:  # 跳过1分钟前的历史消息
         if conf().get("hot_reload") == True and int(create_time) < int(time.time()) - 60:  # 跳过1分钟前的历史消息
             logger.debug("[WX]history message {} skipped".format(msgId))
             logger.debug("[WX]history message {} skipped".format(msgId))
             return
             return
-        if cmsg.my_msg:
+        if cmsg.my_msg and not cmsg.is_group:
             logger.debug("[WX]my message {} skipped".format(msgId))
             logger.debug("[WX]my message {} skipped".format(msgId))
             return
             return
         return func(self, cmsg)
         return func(self, cmsg)

+ 5 - 1
channel/wechat/wechat_message.py

@@ -57,7 +57,8 @@ class WechatMessage(ChatMessage):
             self.from_user_nickname = nickname
             self.from_user_nickname = nickname
         if self.to_user_id == user_id:
         if self.to_user_id == user_id:
             self.to_user_nickname = nickname
             self.to_user_nickname = nickname
-        try:  # 陌生人时候, 'User'字段可能不存在
+        try:  # 陌生人时候, User字段可能不存在
+            # my_msg 为True是表示是自己发送的消息
             self.my_msg = itchat_msg["ToUserName"] == itchat_msg["User"]["UserName"] and \
             self.my_msg = itchat_msg["ToUserName"] == itchat_msg["User"]["UserName"] and \
                           itchat_msg["ToUserName"] != itchat_msg["FromUserName"]
                           itchat_msg["ToUserName"] != itchat_msg["FromUserName"]
             self.other_user_id = itchat_msg["User"]["UserName"]
             self.other_user_id = itchat_msg["User"]["UserName"]
@@ -66,6 +67,9 @@ class WechatMessage(ChatMessage):
                 self.from_user_nickname = self.other_user_nickname
                 self.from_user_nickname = self.other_user_nickname
             if self.other_user_id == self.to_user_id:
             if self.other_user_id == self.to_user_id:
                 self.to_user_nickname = self.other_user_nickname
                 self.to_user_nickname = self.other_user_nickname
+            if itchat_msg["User"].get("Self"):
+                # 自身的展示名,当设置了群昵称时,该字段表示群昵称
+                self.self_display_name = itchat_msg["User"].get("Self").get("DisplayName")
         except KeyError as e:  # 处理偶尔没有对方信息的情况
         except KeyError as e:  # 处理偶尔没有对方信息的情况
             logger.warn("[WX]get other_user_id failed: " + str(e))
             logger.warn("[WX]get other_user_id failed: " + str(e))
             if self.from_user_id == user_id:
             if self.from_user_id == user_id:

+ 1 - 1
plugins/linkai/README.md

@@ -33,7 +33,7 @@
 
 
 ## 插件使用
 ## 插件使用
 
 
-> 使用插件中的知识库管理功能需要首先开启`linkai`对话,依赖全局 `config.json` 中的 `use_linkai` 和 `linkai_api_key` 配置;而midjourney绘画功能则只需填写 `linkai_api_key` 配置。具体可参考 [详细文档](https://link-ai.tech/platform/link-app/wechat)。
+> 使用插件中的知识库管理功能需要首先开启`linkai`对话,依赖全局 `config.json` 中的 `use_linkai` 和 `linkai_api_key` 配置;而midjourney绘画功能则只需填写 `linkai_api_key` 配置,`use_linkai` 无论是否关闭均可使用。具体可参考 [详细文档](https://link-ai.tech/platform/link-app/wechat)。
 
 
 完成配置后运行项目,会自动运行插件,输入 `#help linkai` 可查看插件功能。
 完成配置后运行项目,会自动运行插件,输入 `#help linkai` 可查看插件功能。
 
 

+ 36 - 14
plugins/linkai/linkai.py

@@ -1,19 +1,10 @@
-import asyncio
-import json
-import threading
-from concurrent.futures import ThreadPoolExecutor
-
 import plugins
 import plugins
 from bridge.context import ContextType
 from bridge.context import ContextType
 from bridge.reply import Reply, ReplyType
 from bridge.reply import Reply, ReplyType
-from channel.chat_message import ChatMessage
-from common.log import logger
-from config import conf, global_config
+from config import global_config
 from plugins import *
 from plugins import *
-from .midjourney import MJBot, TaskType
-
-# 任务线程池
-task_thread_pool = ThreadPoolExecutor(max_workers=4)
+from .midjourney import MJBot
+from bridge import bridge
 
 
 
 
 @plugins.register(
 @plugins.register(
@@ -66,11 +57,28 @@ class LinkAI(Plugin):
         if len(cmd) == 1 or (len(cmd) == 2 and cmd[1] == "help"):
         if len(cmd) == 1 or (len(cmd) == 2 and cmd[1] == "help"):
             _set_reply_text(self.get_help_text(verbose=True), e_context, level=ReplyType.INFO)
             _set_reply_text(self.get_help_text(verbose=True), e_context, level=ReplyType.INFO)
             return
             return
+
+        if len(cmd) == 2 and (cmd[1] == "open" or cmd[1] == "close"):
+            # 知识库开关指令
+            if not _is_admin(e_context):
+                _set_reply_text("需要管理员权限执行", e_context, level=ReplyType.ERROR)
+                return
+            is_open = True
+            tips_text = "开启"
+            if cmd[1] == "close":
+                tips_text = "关闭"
+                is_open = False
+            conf()["use_linkai"] = is_open
+            bridge.Bridge().reset_bot()
+            _set_reply_text(f"知识库功能已{tips_text}", e_context, level=ReplyType.INFO)
+            return
+
         if len(cmd) == 3 and cmd[1] == "app":
         if len(cmd) == 3 and cmd[1] == "app":
+            # 知识库应用切换指令
             if not context.kwargs.get("isgroup"):
             if not context.kwargs.get("isgroup"):
                 _set_reply_text("该指令需在群聊中使用", e_context, level=ReplyType.ERROR)
                 _set_reply_text("该指令需在群聊中使用", e_context, level=ReplyType.ERROR)
                 return
                 return
-            if context.kwargs.get("msg").actual_user_id not in global_config["admin_users"]:
+            if not _is_admin(e_context):
                 _set_reply_text("需要管理员权限执行", e_context, level=ReplyType.ERROR)
                 _set_reply_text("需要管理员权限执行", e_context, level=ReplyType.ERROR)
                 return
                 return
             app_code = cmd[2]
             app_code = cmd[2]
@@ -84,7 +92,8 @@ class LinkAI(Plugin):
             super().save_config(self.config)
             super().save_config(self.config)
             _set_reply_text(f"应用设置成功: {app_code}", e_context, level=ReplyType.INFO)
             _set_reply_text(f"应用设置成功: {app_code}", e_context, level=ReplyType.INFO)
         else:
         else:
-            _set_reply_text(f"指令错误,请输入{_get_trigger_prefix()}linkai help 获取帮助", e_context, level=ReplyType.INFO)
+            _set_reply_text(f"指令错误,请输入{_get_trigger_prefix()}linkai help 获取帮助", e_context,
+                            level=ReplyType.INFO)
             return
             return
 
 
     # LinkAI 对话任务处理
     # LinkAI 对话任务处理
@@ -127,6 +136,19 @@ class LinkAI(Plugin):
 
 
 
 
 # 静态方法
 # 静态方法
+def _is_admin(e_context: EventContext) -> bool:
+    """
+    判断消息是否由管理员用户发送
+    :param e_context: 消息上下文
+    :return: True: 是, False: 否
+    """
+    context = e_context["context"]
+    if context["isgroup"]:
+        return context.kwargs.get("msg").actual_user_id in global_config["admin_users"]
+    else:
+        return context["receiver"] in global_config["admin_users"]
+
+
 def _set_reply_text(content: str, e_context: EventContext, level: ReplyType = ReplyType.ERROR):
 def _set_reply_text(content: str, e_context: EventContext, level: ReplyType = ReplyType.ERROR):
     reply = Reply(level, content)
     reply = Reply(level, content)
     e_context["reply"] = reply
     e_context["reply"] = reply

+ 18 - 1
plugins/linkai/midjourney.py

@@ -69,7 +69,7 @@ class MJBot:
         :param e_context: 上下文
         :param e_context: 上下文
         :return: 任务类型枚举
         :return: 任务类型枚举
         """
         """
-        if not self.config or not self.config.get("enabled"):
+        if not self.config:
             return None
             return None
         trigger_prefix = conf().get("plugin_trigger_prefix", "$")
         trigger_prefix = conf().get("plugin_trigger_prefix", "$")
         context = e_context['context']
         context = e_context['context']
@@ -92,9 +92,26 @@ class MJBot:
         session_id = context["session_id"]
         session_id = context["session_id"]
         cmd = context.content.split(maxsplit=1)
         cmd = context.content.split(maxsplit=1)
         if len(cmd) == 1 and context.type == ContextType.TEXT:
         if len(cmd) == 1 and context.type == ContextType.TEXT:
+            # midjourney 帮助指令
             self._set_reply_text(self.get_help_text(verbose=True), e_context, level=ReplyType.INFO)
             self._set_reply_text(self.get_help_text(verbose=True), e_context, level=ReplyType.INFO)
             return
             return
 
 
+        if len(cmd) == 2 and (cmd[1] == "open" or cmd[1] == "close"):
+            # midjourney 开关指令
+            is_open = True
+            tips_text = "开启"
+            if cmd[1] == "close":
+                tips_text = "关闭"
+                is_open = False
+            self.config["enabled"] = is_open
+            self._set_reply_text(f"Midjourney绘画已{tips_text}", e_context, level=ReplyType.INFO)
+            return
+
+        if not self.config.get("enabled"):
+            logger.warn("Midjourney绘画未开启,请查看 plugins/linkai/config.json 中的配置")
+            self._set_reply_text(f"Midjourney绘画未开启", e_context, level=ReplyType.INFO)
+            return
+
         if not self._check_rate_limit(session_id, e_context):
         if not self._check_rate_limit(session_id, e_context):
             logger.warn("[MJ] midjourney task exceed rate limit")
             logger.warn("[MJ] midjourney task exceed rate limit")
             return
             return

+ 1 - 1
plugins/plugin.py

@@ -19,7 +19,7 @@ class Plugin:
             # 全局配置不存在 或者 未开启全局配置开关,则获取插件目录下的配置
             # 全局配置不存在 或者 未开启全局配置开关,则获取插件目录下的配置
             plugin_config_path = os.path.join(self.path, "config.json")
             plugin_config_path = os.path.join(self.path, "config.json")
             if os.path.exists(plugin_config_path):
             if os.path.exists(plugin_config_path):
-                with open(plugin_config_path, "r") as f:
+                with open(plugin_config_path, "r", encoding="utf-8") as f:
                     plugin_conf = json.load(f)
                     plugin_conf = json.load(f)
         logger.debug(f"loading plugin config, plugin_name={self.name}, conf={plugin_conf}")
         logger.debug(f"loading plugin config, plugin_name={self.name}, conf={plugin_conf}")
         return plugin_conf
         return plugin_conf