diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..f2bca1fe Binary files /dev/null and b/requirements.txt differ diff --git a/zhenxun/plugins/about.py b/zhenxun/builtin_plugins/about.py similarity index 100% rename from zhenxun/plugins/about.py rename to zhenxun/builtin_plugins/about.py diff --git a/zhenxun/plugins/auto_update/__init__.py b/zhenxun/builtin_plugins/auto_update/__init__.py similarity index 100% rename from zhenxun/plugins/auto_update/__init__.py rename to zhenxun/builtin_plugins/auto_update/__init__.py diff --git a/zhenxun/plugins/auto_update/_data_source.py b/zhenxun/builtin_plugins/auto_update/_data_source.py similarity index 99% rename from zhenxun/plugins/auto_update/_data_source.py rename to zhenxun/builtin_plugins/auto_update/_data_source.py index 2f1c0276..5f497bdf 100644 --- a/zhenxun/plugins/auto_update/_data_source.py +++ b/zhenxun/builtin_plugins/auto_update/_data_source.py @@ -98,7 +98,7 @@ def _file_handle(latest_version: str | None): if latest_version: with open(VERSION_FILE, "w", encoding="utf8") as f: f.write(f"__version__: {latest_version}") - os.system(f"poetry install --directory={Path().absolute()}") + os.system(f"poetry run pip install -r requirements.txt") class UpdateManage: diff --git a/zhenxun/plugins/auto_update/config.py b/zhenxun/builtin_plugins/auto_update/config.py similarity index 100% rename from zhenxun/plugins/auto_update/config.py rename to zhenxun/builtin_plugins/auto_update/config.py diff --git a/zhenxun/builtin_plugins/help/__init__.py b/zhenxun/builtin_plugins/help/__init__.py index 69a981a3..3f89c117 100644 --- a/zhenxun/builtin_plugins/help/__init__.py +++ b/zhenxun/builtin_plugins/help/__init__.py @@ -67,6 +67,7 @@ async def _( session: EventSession, is_superuser: Query[bool] = AlconnaQuery("superuser.value", False), ): + logger.debug("进入help") _is_superuser = False if is_superuser.available: _is_superuser = is_superuser.result diff --git a/zhenxun/builtin_plugins/plugin_shop/__init__.py b/zhenxun/builtin_plugins/plugin_shop/__init__.py new file mode 100644 index 00000000..5e6c12a2 --- /dev/null +++ b/zhenxun/builtin_plugins/plugin_shop/__init__.py @@ -0,0 +1,89 @@ +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Subcommand, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils + +from .data_source import ShopManage + +__plugin_meta__ = PluginMetadata( + name="插件商店", + description="插件商店", + usage=""" + 插件商店 : 查看当前的插件商店 + 添加插件 id : 添加插件 + 移除插件 id : 移除插件 + + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPERUSER, + ).dict(), +) + +_matcher = on_alconna( + Alconna( + "插件商店", + Subcommand("add", Args["plugin_id", int]), + Subcommand("remove", Args["plugin_id", int]), + ), + permission=SUPERUSER, + priority=1, + block=True, +) + + +_matcher.shortcut( + r"添加插件", + command="插件商店", + arguments=["add", "{%0}"], + prefix=True, +) + +_matcher.shortcut( + r"移除插件", + command="插件商店", + arguments=["remove", "{%0}"], + prefix=True, +) + + +@_matcher.assign("$main") +async def _(session: EventSession): + try: + result = await ShopManage.get_plugins_info() + logger.info("查看插件列表", "插件商店", session=session) + await MessageUtils.build_message(result).finish() + except Exception as e: + logger.error(f"查看插件列表失败 e: {e}", "插件商店", session=session, e=e) + + +@_matcher.assign("add") +async def _(session: EventSession, plugin_id: int): + try: + result = await ShopManage.add_plugin(plugin_id) + except Exception as e: + logger.error(f"添加插件 Id: {plugin_id}失败", "插件商店", session=session, e=e) + await MessageUtils.build_message( + f"添加插件 Id: {plugin_id} 失败 e: {e}" + ).finish() + logger.info(f"添加插件 Id: {plugin_id}", "插件商店", session=session) + await MessageUtils.build_message(result).finish() + + +@_matcher.assign("remove") +async def _(session: EventSession, plugin_id: int): + try: + result = await ShopManage.remove_plugin(plugin_id) + except Exception as e: + logger.error(f"移除插件 Id: {plugin_id}失败", "插件商店", session=session, e=e) + await MessageUtils.build_message( + f"移除插件 Id: {plugin_id} 失败 e: {e}" + ).finish() + logger.info(f"移除插件 Id: {plugin_id}", "插件商店", session=session) + await MessageUtils.build_message(result).finish() diff --git a/zhenxun/builtin_plugins/plugin_shop/config.py b/zhenxun/builtin_plugins/plugin_shop/config.py new file mode 100644 index 00000000..5128128f --- /dev/null +++ b/zhenxun/builtin_plugins/plugin_shop/config.py @@ -0,0 +1,15 @@ +from pathlib import Path + +BASE_PATH = Path() / "zhenxun" +BASE_PATH.mkdir(parents=True, exist_ok=True) + + +CONFIG_URL = ( + "https://raw.githubusercontent.com/HibiKier/zhenxun_bot_plugins/main/plugins.json" +) +"""插件信息文件""" + +DOWNLOAD_URL = ( + "https://api.github.com/repos/HibiKier/zhenxun_bot_plugins/contents/{}?ref=main" +) +"""插件下载地址""" diff --git a/zhenxun/builtin_plugins/plugin_shop/data_source.py b/zhenxun/builtin_plugins/plugin_shop/data_source.py new file mode 100644 index 00000000..324104e5 --- /dev/null +++ b/zhenxun/builtin_plugins/plugin_shop/data_source.py @@ -0,0 +1,195 @@ +import shutil +from pathlib import Path + +import nonebot +import ujson as json + +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.image_utils import BuildImage, ImageTemplate, RowStyle + +from .config import BASE_PATH, CONFIG_URL, DOWNLOAD_URL + + +def row_style(column: str, text: str) -> RowStyle: + """被动技能文本风格 + + 参数: + column: 表头 + text: 文本内容 + + 返回: + RowStyle: RowStyle + """ + style = RowStyle() + if column in ["-"]: + if text == "已安装": + style.font_color = "#67C23A" + return style + + +async def recurrence_get_url( + url: str, data_list: list[tuple[str, str]], ignore_list: list[str] = [] +): + """递归获取目录下所有文件 + + 参数: + url: 信息url + data_list: 数据列表 + + 异常: + ValueError: 访问错误 + """ + logger.debug(f"访问插件下载信息 URL: {url}", "插件管理") + res = await AsyncHttpx.get(url) + if res.status_code != 200: + raise ValueError(f"访问错误, code: {res.status_code}") + json_data = res.json() + if isinstance(json_data, list): + for v in json_data: + data_list.append((v.get("download_url"), v["path"])) + else: + data_list.append((json_data.get("download_url"), json_data["path"])) + for download_url, path in data_list: + if not download_url: + _url = DOWNLOAD_URL.format(path) + if _url not in ignore_list: + ignore_list.append(_url) + await recurrence_get_url(_url, data_list, ignore_list) + + +async def download_file(url: str): + """下载文件 + + 参数: + url: 插件详情url + + 异常: + ValueError: 访问失败 + ValueError: 下载失败 + """ + data_list = [] + await recurrence_get_url(url, data_list) + for download_url, path in data_list: + if download_url and "." in path: + logger.debug(f"下载文件: {path}", "插件管理") + file = Path(f"zhenxun/{path}") + file.parent.mkdir(parents=True, exist_ok=True) + r = await AsyncHttpx.get(download_url) + if r.status_code != 200: + raise ValueError(f"文件下载错误, code: {r.status_code}") + with open(file, "w", encoding="utf8") as f: + logger.debug(f"写入文件: {file}", "插件管理") + f.write(r.text) + + +class ShopManage: + + type2name = { + "NORMAL": "普通插件", + "ADMIN": "管理员插件", + "SUPERUSER": "超级用户插件", + "ADMIN_SUPERUSER": "管理员/超级用户插件", + "DEPENDANT": "依赖插件", + "HIDDEN": "其他插件", + } + + @classmethod + async def __get_data(cls) -> dict: + """获取插件信息数据 + + 异常: + ValueError: 访问请求失败 + + 返回: + dict: 插件信息数据 + """ + res = await AsyncHttpx.get(CONFIG_URL) + if res.status_code != 200: + raise ValueError(f"下载错误, code: {res.status_code}") + return json.loads(res.text) + + @classmethod + async def get_plugins_info(cls) -> BuildImage | str: + """插件列表 + + 返回: + BuildImage | str: 返回消息 + """ + data: dict = await cls.__get_data() + column_name = ["-", "ID", "名称", "简介", "作者", "版本", "类型"] + for k in data.copy(): + if data[k]["plugin_type"]: + data[k]["plugin_type"] = cls.type2name[data[k]["plugin_type"]] + suc_plugin = [p.name for p in nonebot.get_loaded_plugins()] + data_list = [ + [ + "已安装" if v[1]["module"] in suc_plugin else "", + i, + v[0], + v[1]["description"], + v[1]["author"], + v[1]["version"], + v[1]["plugin_type"], + ] + for i, v in enumerate(data.items()) + ] + return await ImageTemplate.table_page( + "插件列表", + f"通过安装/卸载插件 ID 来管理插件", + column_name, + data_list, + text_style=row_style, + ) + + @classmethod + async def add_plugin(cls, plugin_id: int) -> str: + data: dict = await cls.__get_data() + if plugin_id < 0 or plugin_id >= len(data): + return "插件ID不存在..." + plugin_key = list(data.keys())[plugin_id] + plugin_info = data[plugin_key] + module_path_split = plugin_info["module_path"].split(".") + url_path = None + path = BASE_PATH + if len(module_path_split) == 2: + """单个文件或文件夹""" + if plugin_info["is_dir"]: + url_path = "/".join(module_path_split) + else: + url_path = "/".join(module_path_split) + ".py" + else: + """嵌套文件或文件夹""" + for p in module_path_split[:-1]: + path = path / p + path.mkdir(parents=True, exist_ok=True) + if plugin_info["is_dir"]: + url_path = f"{'/'.join(module_path_split)}" + else: + url_path = f"{'/'.join(module_path_split)}.py" + if not url_path: + return "插件下载地址构建失败..." + logger.debug(f"尝试下载插件 URL: {url_path}", "插件管理") + await download_file(DOWNLOAD_URL.format(url_path)) + return f"插件 {plugin_key} 安装成功!" + + @classmethod + async def remove_plugin(cls, plugin_id: int) -> str: + data: dict = await cls.__get_data() + if plugin_id < 0 or plugin_id >= len(data): + return "插件ID不存在..." + plugin_key = list(data.keys())[plugin_id] + plugin_info = data[plugin_key] + path = BASE_PATH + for p in plugin_info["module_path"].split("."): + path = path / p + if not plugin_info["is_dir"]: + path = Path(f"{path}.py") + if not path.exists(): + return f"插件 {plugin_key} 不存在..." + logger.debug(f"尝试移除插件 {plugin_key} 文件: {path}", "插件管理") + if plugin_info["is_dir"]: + shutil.rmtree(path) + else: + path.unlink() + return f"插件 {plugin_key} 移除成功!" diff --git a/zhenxun/plugins/statistics/__init__.py b/zhenxun/builtin_plugins/statistics/__init__.py similarity index 100% rename from zhenxun/plugins/statistics/__init__.py rename to zhenxun/builtin_plugins/statistics/__init__.py diff --git a/zhenxun/plugins/statistics/_data_source.py b/zhenxun/builtin_plugins/statistics/_data_source.py similarity index 100% rename from zhenxun/plugins/statistics/_data_source.py rename to zhenxun/builtin_plugins/statistics/_data_source.py diff --git a/zhenxun/plugins/statistics/statistics_handle.py b/zhenxun/builtin_plugins/statistics/statistics_handle.py similarity index 100% rename from zhenxun/plugins/statistics/statistics_handle.py rename to zhenxun/builtin_plugins/statistics/statistics_handle.py diff --git a/zhenxun/plugins/statistics/statistics_hook.py b/zhenxun/builtin_plugins/statistics/statistics_hook.py similarity index 100% rename from zhenxun/plugins/statistics/statistics_hook.py rename to zhenxun/builtin_plugins/statistics/statistics_hook.py diff --git a/zhenxun/configs/config.py b/zhenxun/configs/config.py index e1140a88..0206c131 100644 --- a/zhenxun/configs/config.py +++ b/zhenxun/configs/config.py @@ -19,7 +19,7 @@ NICKNAME: str = "小真寻" # 数据库(必要) # 如果填写了bind就不需要再填写后面的字段了#) # 示例:"bind": "postgres://user:password@127.0.0.1:5432/database" -bind: str = "" # 数据库连接链接 +bind: str = "" sql_name: str = "postgres" user: str = "" # 数据用户名 password: str = "" # 数据库密码 @@ -29,7 +29,7 @@ database: str = "" # 数据库名称 # 代理,例如 "http://127.0.0.1:7890" # 如果是WLS 可以 f"http://{hostip}:7890" 使用寄主机的代理 -SYSTEM_PROXY: str | None = None # 全局代理 +SYSTEM_PROXY: str | None = "http://127.0.0.1:7890" # 全局代理 Config = ConfigsManager(Path() / "data" / "configs" / "plugins2config.yaml")