Browse Source

feat: add installp/uninstallp command

lanvent 3 years ago
parent
commit
3c6d42b22e

+ 15 - 0
common/package_manager.py

@@ -0,0 +1,15 @@
+import pip
+
+def install(package):
+    pip.main(['install', package])
+
+def install_requirements(file):
+    pip.main(['install', '-r', file, "--upgrade"])
+
+def check_dulwich():
+    try:
+        import dulwich
+        return
+    except ImportError:
+        install('dulwich')
+    raise ImportError("Unable to import dulwich")

+ 1 - 0
plugins/banwords/__init__.py

@@ -0,0 +1 @@
+from .banwords import *

+ 1 - 0
plugins/bdunit/__init__.py

@@ -0,0 +1 @@
+from .bdunit import *

+ 1 - 0
plugins/dungeon/__init__.py

@@ -0,0 +1 @@
+from .dungeon import *

+ 1 - 0
plugins/finish/__init__.py

@@ -0,0 +1 @@
+from .finish import *

+ 1 - 0
plugins/godcmd/__init__.py

@@ -0,0 +1 @@
+from .godcmd import *

+ 30 - 7
plugins/godcmd/godcmd.py

@@ -37,10 +37,10 @@ COMMANDS = {
         "alias": ["reset_openai_api_key"],
         "desc": "重置为默认的api_key",
     },
-    # "id": {
-    #     "alias": ["id", "用户"],
-    #     "desc": "获取用户id", #目前无实际意义
-    # },
+    "id": {
+        "alias": ["id", "用户"],
+        "desc": "获取用户id", # wechaty和wechatmp的用户id不会变化,可用于绑定管理员
+    },
     "reset": {
         "alias": ["reset", "重置会话"],
         "desc": "重置会话",
@@ -92,6 +92,16 @@ ADMIN_COMMANDS = {
         "args": ["插件名"],
         "desc": "禁用指定插件",
     },
+    "installp": {
+        "alias": ["installp", "安装插件"],
+        "args": ["仓库地址或插件名"],
+        "desc": "安装指定插件",
+    },
+    "uninstallp": {
+        "alias": ["uninstallp", "卸载插件"],
+        "args": ["插件名"],
+        "desc": "卸载指定插件",
+    },
     "debug": {
         "alias": ["debug", "调试模式", "DEBUG"],
         "desc": "开启机器调试日志",
@@ -103,7 +113,9 @@ def get_help_text(isadmin, isgroup):
     for cmd, info in COMMANDS.items():
         if cmd=="auth": #不提示认证指令
             continue
-        alias=["#"+a for a in info['alias']]
+        if cmd=="id" and conf().get("channel_type","wx") not in ["wxy","wechatmp"]:
+            continue
+        alias=["#"+a for a in info['alias'][:1]]
         help_text += f"{','.join(alias)} "
         if 'args' in info:
             args=[a for a in info['args']]
@@ -122,7 +134,7 @@ def get_help_text(isadmin, isgroup):
     if ADMIN_COMMANDS and isadmin:
         help_text += "\n\n管理员指令:\n"
         for cmd, info in ADMIN_COMMANDS.items():
-            alias=["#"+a for a in info['alias']]
+            alias=["#"+a for a in info['alias'][:1]]
             help_text += f"{','.join(alias)} "
             if 'args' in info:
                 args=[a for a in info['args']]
@@ -208,6 +220,8 @@ class Godcmd(Plugin):
                                 break
                         if not ok:
                             result = "插件不存在或未启用"
+                elif cmd == "id":
+                    ok, result = True, user
                 elif cmd == "set_openai_api_key":
                     if len(args) == 1:
                         user_data = conf().get_user_data(user)
@@ -310,7 +324,16 @@ class Godcmd(Plugin):
                                     result = "插件已禁用"
                                 else:
                                     result = "插件不存在"
-
+                        elif cmd == "installp":
+                            if len(args) != 1:
+                                ok, result = False, "请提供插件名或.git结尾的仓库地址"
+                            else:
+                                ok, result = PluginManager().install_plugin(args[0])
+                        elif cmd == "uninstallp":
+                            if len(args) != 1:
+                                ok, result = False, "请提供插件名"
+                            else:
+                                ok, result = PluginManager().uninstall_plugin(args[0])
                         logger.debug("[Godcmd] admin command: %s by %s" % (cmd, user))
                 else:
                     ok, result = False, "需要管理员权限才能执行该指令"

+ 1 - 0
plugins/hello/__init__.py

@@ -0,0 +1 @@
+from .hello import *

+ 63 - 5
plugins/plugin_manager.py

@@ -17,6 +17,7 @@ class PluginManager:
         self.listening_plugins = {}
         self.instances = {}
         self.pconf = {}
+        self.current_plugin_path = None
 
     def register(self, name: str, desire_priority: int = 0, **kwargs):
         def wrapper(plugincls):
@@ -24,12 +25,13 @@ class PluginManager:
             plugincls.priority = desire_priority
             plugincls.desc = kwargs.get('desc')
             plugincls.author = kwargs.get('author')
+            plugincls.path = self.current_plugin_path
             plugincls.version = kwargs.get('version') if kwargs.get('version') != None else "1.0"
             plugincls.namecn = kwargs.get('namecn') if kwargs.get('namecn') != None else name
             plugincls.hidden = kwargs.get('hidden') if kwargs.get('hidden') != None else False
             plugincls.enabled = True
             self.plugins[name.upper()] = plugincls
-            logger.info("Plugin %s_v%s registered" % (name, plugincls.version))
+            logger.info("Plugin %s_v%s registered, path=%s" % (name, plugincls.version, plugincls.path))
             return plugincls
         return wrapper
 
@@ -59,12 +61,13 @@ class PluginManager:
         for plugin_name in os.listdir(plugins_dir):
             plugin_path = os.path.join(plugins_dir, plugin_name)
             if os.path.isdir(plugin_path):
-                # 判断插件是否包含同名.py文件
-                main_module_path = os.path.join(plugin_path, plugin_name+".py")
+                # 判断插件是否包含同名__init__.py文件
+                main_module_path = os.path.join(plugin_path,"__init__.py")
                 if os.path.isfile(main_module_path):
                     # 导入插件
-                    import_path = "plugins.{}.{}".format(plugin_name, plugin_name)
+                    import_path = "plugins.{}".format(plugin_name)
                     try:
+                        self.current_plugin_path = plugin_path
                         main_module = importlib.import_module(import_path)
                     except Exception as e:
                         logger.warn("Failed to import plugin %s: %s" % (plugin_name, e))
@@ -179,4 +182,59 @@ class PluginManager:
         return True
     
     def list_plugins(self):
-        return self.plugins
+        return self.plugins
+    
+    def install_plugin(self, repo:str):
+        try:
+            import common.package_manager as pkgmgr
+            pkgmgr.check_dulwich()
+        except Exception as e:
+            logger.error("Failed to install plugin, {}".format(e))
+            return False, "无法导入dulwich,安装插件失败"
+        import re
+        from dulwich import porcelain
+
+        logger.info("clone git repo: {}".format(repo))
+        
+        match = re.match(r"^(https?:\/\/|git@)([^\/:]+)[\/:]([^\/:]+)\/(.+).git$", repo)
+        
+        if not match:
+            try:
+                with open("./plugins/source.json","r") as f:
+                    source = json.load(f)
+                if repo in source["repo"]:
+                    repo = source["repo"][repo]["url"]
+                    match = re.match(r"^(https?:\/\/|git@)([^\/:]+)[\/:]([^\/:]+)\/(.+).git$", repo)
+                    if not match:
+                        return False, "source中的仓库地址不合法"
+                else:
+                    return False, "仓库地址不合法"
+            except Exception as e:
+                logger.error("Failed to install plugin, {}".format(e))
+                return False, "安装插件失败"
+        dirname = os.path.join("./plugins",match.group(4))
+        try:
+            repo = porcelain.clone(repo, dirname, checkout=True)
+            if os.path.exists(os.path.join(dirname,"requirements.txt")):
+                logger.info("detect requirements.txt,installing...")
+            pkgmgr.install_requirements(os.path.join(dirname,"requirements.txt"))
+            return True, "安装插件成功,请扫描插件或重启程序"
+        except Exception as e:
+            logger.error("Failed to install plugin, {}".format(e))
+            return False, "安装插件失败"
+        
+    def uninstall_plugin(self, name:str):
+        name = name.upper()
+        if name not in self.plugins:
+            return False, "插件不存在"
+        if name in self.instances:
+            self.disable_plugin(name)
+        dirname = self.plugins[name].path
+        try:
+            import shutil
+            shutil.rmtree(dirname)
+            del self.plugins[name]
+            return True, "卸载插件成功"
+        except Exception as e:
+            logger.error("Failed to uninstall plugin, {}".format(e))
+            return False, "卸载插件失败"

+ 1 - 0
plugins/role/__init__.py

@@ -0,0 +1 @@
+from .role import *

+ 7 - 0
plugins/source.json

@@ -0,0 +1,7 @@
+{
+    "repo": {
+        "sdwebui": {
+            "url": "https://github.com/lanvent/plugin_sdwebui.git"
+        }
+    }
+}

+ 1 - 0
plugins/tool/__init__.py

@@ -0,0 +1 @@
+from .tool import *

+ 3 - 0
requirements-optional.txt

@@ -8,6 +8,9 @@ pyttsx3>=2.90 # pytsx text to speech
 baidu_aip>=4.16.10 # baidu voice
 # azure-cognitiveservices-speech # azure voice
 
+#install plugin
+dulwich
+
 # wechaty
 wechaty>=0.10.7
 wechaty_puppet>=0.4.23