This commit is contained in:
HibiKier
2025-08-04 15:36:39 +00:00
committed by GitHub
33 changed files with 3486 additions and 811 deletions
+3 -2
View File
@@ -17,7 +17,7 @@ from zhenxun.models.user_console import UserConsole
from zhenxun.services.log import logger
from zhenxun.utils.decorator.shop import shop_register
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
from zhenxun.utils.manager.resource_manager import ResourceManager
from zhenxun.utils.manager.zhenxun_repo_manager import ZhenxunRepoManager
from zhenxun.utils.platform import PlatformUtils
driver: Driver = nonebot.get_driver()
@@ -85,7 +85,8 @@ from bag_users t1
@PriorityLifecycle.on_startup(priority=5)
async def _():
await ResourceManager.init_resources()
if not ZhenxunRepoManager.check_resources_exists():
await ZhenxunRepoManager.resources_update(branch="test")
"""签到与用户的数据迁移"""
if goods_list := await GoodsInfo.filter(uuid__isnull=True).all():
for goods in goods_list:
@@ -104,25 +104,16 @@ class MemberUpdateManage:
exist_member_list.append(member.id)
if data_list[0]:
try:
await GroupInfoUser.bulk_create(data_list[0], 30)
await GroupInfoUser.bulk_create(
data_list[0], 30, ignore_conflicts=True
)
logger.debug(
f"创建用户数据 {len(data_list[0])} 条",
"更新群组成员信息",
target=group_id,
)
except Exception as e:
logger.error(
f"批量创建用户数据失败: {e},开始进行逐个存储",
"更新群组成员信息",
)
for u in data_list[0]:
try:
await u.save()
except Exception as e:
logger.error(
f"创建用户 {u.user_name}({u.user_id}) 数据失败: {e}",
"更新群组成员信息",
)
logger.error("批量创建用户数据失败", "更新群组成员信息", e=e)
if data_list[1]:
await GroupInfoUser.bulk_update(data_list[1], ["user_name"], 30)
logger.debug(
+46 -14
View File
@@ -16,10 +16,6 @@ from nonebot_plugin_uninfo import Uninfo
from zhenxun.configs.utils import PluginExtraData
from zhenxun.services.log import logger
from zhenxun.utils.enum import PluginType
from zhenxun.utils.manager.resource_manager import (
DownloadResourceException,
ResourceManager,
)
from zhenxun.utils.message import MessageUtils
from ._data_source import UpdateManager
@@ -32,15 +28,23 @@ __plugin_meta__ = PluginMetadata(
检查更新真寻最新版本,包括了自动更新
资源文件大小一般在130mb左右,除非必须更新一般仅更新代码文件
指令:
检查更新 [main|release|resource|webui] ?[-r]
检查更新 [main|release|resource|webui] ?[-r] ?[-f] ?[-z] ?[-t]
main: main分支
release: 最新release
resource: 资源文件
webui: webui文件
-r: 下载资源文件,一般在更新main或release时使用
-f: 强制更新,一般用于更新main时使用(仅git更新时有效)
-s: 更新源,为 git 或 ali(默认使用ali)
-z: 下载zip文件进行更新(仅git有效)
-t: 更新方式,git或download(默认使用git)
git: 使用git pull(推荐)
download: 通过commit hash比较文件后下载更新(仅git有效)
示例:
检查更新 main
检查更新 main -r
检查更新 main -f
检查更新 release -r
检查更新 resource
检查更新 webui
@@ -57,6 +61,9 @@ _matcher = on_alconna(
"检查更新",
Args["ver_type?", ["main", "release", "resource", "webui"]],
Option("-r|--resource", action=store_true, help_text="下载资源文件"),
Option("-f|--force", action=store_true, help_text="强制更新"),
Option("-s", Args["source?", ["git", "ali"]], help_text="更新源"),
Option("-z|--zip", action=store_true, help_text="下载zip文件"),
),
priority=1,
block=True,
@@ -71,30 +78,55 @@ async def _(
session: Uninfo,
ver_type: Match[str],
resource: Query[bool] = Query("resource", False),
force: Query[bool] = Query("force", False),
source: Query[str] = Query("source", "ali"),
zip: Query[bool] = Query("zip", False),
):
result = ""
await MessageUtils.build_message("正在进行检查更新...").send(reply_to=True)
if ver_type.result in {"main", "release"}:
ver_type_str = ver_type.result
source_str = source.result
if ver_type_str in {"main", "release"}:
if not ver_type.available:
result = await UpdateManager.check_version()
result += await UpdateManager.check_version()
logger.info("查看当前版本...", "检查更新", session=session)
await MessageUtils.build_message(result).finish()
try:
result = await UpdateManager.update(bot, session.user.id, ver_type.result)
result += await UpdateManager.update_zhenxun(
bot,
session.user.id,
ver_type_str, # type: ignore
force.result,
source_str, # type: ignore
zip.result,
)
await MessageUtils.build_message(result).finish(reply_to=True)
except Exception as e:
logger.error("版本更新失败...", "检查更新", session=session, e=e)
await MessageUtils.build_message(f"更新版本失败...e: {e}").finish()
elif ver_type.result == "webui":
result = await UpdateManager.update_webui()
if zip.result:
source_str = None
try:
result += await UpdateManager.update_webui(
source_str, # type: ignore
"dist",
)
except Exception as e:
logger.error("WebUI更新失败...", "检查更新", session=session, e=e)
result += "\nWebUI更新错误..."
if resource.result or ver_type.result == "resource":
try:
await ResourceManager.init_resources(True)
result += "\n资源文件更新成功!"
except DownloadResourceException:
result += "\n资源更新下载失败..."
if zip.result:
source_str = None
result += await UpdateManager.update_resources(
source_str, # type: ignore
"main",
force.result,
)
except Exception as e:
logger.error("资源更新下载失败...", "检查更新", session=session, e=e)
result += "\n资源更新未知错误..."
result += "\n资源更新错误..."
if result:
await MessageUtils.build_message(result.strip()).finish()
await MessageUtils.build_message("更新版本失败...").finish()
@@ -1,170 +1,16 @@
import os
import shutil
import subprocess
import tarfile
import zipfile
from typing import Literal
from nonebot.adapters import Bot
from nonebot.utils import run_sync
from zhenxun.configs.path_config import DATA_PATH
from zhenxun.services.log import logger
from zhenxun.utils.github_utils import GithubUtils
from zhenxun.utils.github_utils.models import RepoInfo
from zhenxun.utils.http_utils import AsyncHttpx
from zhenxun.utils.manager.virtual_env_package_manager import VirtualEnvPackageManager
from zhenxun.utils.manager.zhenxun_repo_manager import ZhenxunRepoManager
from zhenxun.utils.platform import PlatformUtils
from .config import (
BACKUP_PATH,
BASE_PATH,
BASE_PATH_STRING,
COMMAND,
DEFAULT_GITHUB_URL,
DOWNLOAD_GZ_FILE,
DOWNLOAD_ZIP_FILE,
PYPROJECT_FILE,
PYPROJECT_FILE_STRING,
PYPROJECT_LOCK_FILE,
PYPROJECT_LOCK_FILE_STRING,
RELEASE_URL,
REPLACE_FOLDERS,
REQ_TXT_FILE,
REQ_TXT_FILE_STRING,
TMP_PATH,
VERSION_FILE,
)
def install_requirement():
requirement_path = (REQ_TXT_FILE).absolute()
if not requirement_path.exists():
logger.debug(
f"没有找到zhenxun的requirement.txt,目标路径为{requirement_path}", COMMAND
)
return
try:
result = subprocess.run(
["pip", "install", "-r", str(requirement_path)],
check=True,
capture_output=True,
text=True,
)
logger.debug(f"成功安装真寻依赖,日志:\n{result.stdout}", COMMAND)
except subprocess.CalledProcessError as e:
logger.error(f"安装真寻依赖失败,错误:\n{e.stderr}", COMMAND, e=e)
@run_sync
def _file_handle(latest_version: str | None):
"""文件移动操作
参数:
latest_version: 版本号
"""
BACKUP_PATH.mkdir(exist_ok=True, parents=True)
logger.debug("开始解压文件压缩包...", COMMAND)
download_file = DOWNLOAD_GZ_FILE
if DOWNLOAD_GZ_FILE.exists():
tf = tarfile.open(DOWNLOAD_GZ_FILE)
else:
download_file = DOWNLOAD_ZIP_FILE
tf = zipfile.ZipFile(DOWNLOAD_ZIP_FILE)
tf.extractall(TMP_PATH)
logger.debug("解压文件压缩包完成...", COMMAND)
download_file_path = TMP_PATH / next(
x for x in os.listdir(TMP_PATH) if (TMP_PATH / x).is_dir()
)
_pyproject = download_file_path / PYPROJECT_FILE_STRING
_lock_file = download_file_path / PYPROJECT_LOCK_FILE_STRING
_req_file = download_file_path / REQ_TXT_FILE_STRING
extract_path = download_file_path / BASE_PATH_STRING
target_path = BASE_PATH
if PYPROJECT_FILE.exists():
logger.debug(f"移除备份文件: {PYPROJECT_FILE}", COMMAND)
shutil.move(PYPROJECT_FILE, BACKUP_PATH / PYPROJECT_FILE_STRING)
if PYPROJECT_LOCK_FILE.exists():
logger.debug(f"移除备份文件: {PYPROJECT_LOCK_FILE}", COMMAND)
shutil.move(PYPROJECT_LOCK_FILE, BACKUP_PATH / PYPROJECT_LOCK_FILE_STRING)
if REQ_TXT_FILE.exists():
logger.debug(f"移除备份文件: {REQ_TXT_FILE}", COMMAND)
shutil.move(REQ_TXT_FILE, BACKUP_PATH / REQ_TXT_FILE_STRING)
if _pyproject.exists():
logger.debug("移动文件: pyproject.toml", COMMAND)
shutil.move(_pyproject, PYPROJECT_FILE)
if _lock_file.exists():
logger.debug("移动文件: poetry.lock", COMMAND)
shutil.move(_lock_file, PYPROJECT_LOCK_FILE)
if _req_file.exists():
logger.debug("移动文件: requirements.txt", COMMAND)
shutil.move(_req_file, REQ_TXT_FILE)
for folder in REPLACE_FOLDERS:
"""移动指定文件夹"""
_dir = BASE_PATH / folder
_backup_dir = BACKUP_PATH / folder
if _backup_dir.exists():
logger.debug(f"删除备份文件夹 {_backup_dir}", COMMAND)
shutil.rmtree(_backup_dir)
if _dir.exists():
logger.debug(f"移动旧文件夹 {_dir}", COMMAND)
shutil.move(_dir, _backup_dir)
else:
logger.warning(f"文件夹 {_dir} 不存在,跳过删除", COMMAND)
for folder in REPLACE_FOLDERS:
src_folder_path = extract_path / folder
dest_folder_path = target_path / folder
if src_folder_path.exists():
logger.debug(
f"移动文件夹: {src_folder_path} -> {dest_folder_path}", COMMAND
)
shutil.move(src_folder_path, dest_folder_path)
else:
logger.debug(f"源文件夹不存在: {src_folder_path}", COMMAND)
if tf:
tf.close()
if download_file.exists():
logger.debug(f"删除下载文件: {download_file}", COMMAND)
download_file.unlink()
if extract_path.exists():
logger.debug(f"删除解压文件夹: {extract_path}", COMMAND)
shutil.rmtree(extract_path)
if TMP_PATH.exists():
shutil.rmtree(TMP_PATH)
if latest_version:
with open(VERSION_FILE, "w", encoding="utf8") as f:
f.write(f"__version__: {latest_version}")
install_requirement()
from .config import LOG_COMMAND, REQUIREMENTS_FILE, VERSION_FILE
class UpdateManager:
@classmethod
async def update_webui(cls) -> str:
from zhenxun.builtin_plugins.web_ui.public.data_source import (
update_webui_assets,
)
WEBUI_PATH = DATA_PATH / "web_ui" / "public"
BACKUP_PATH = DATA_PATH / "web_ui" / "backup_public"
if WEBUI_PATH.exists():
if BACKUP_PATH.exists():
logger.debug(f"删除旧的备份webui文件夹 {BACKUP_PATH}", COMMAND)
shutil.rmtree(BACKUP_PATH)
WEBUI_PATH.rename(BACKUP_PATH)
try:
await update_webui_assets()
logger.info("更新webui成功...", COMMAND)
if BACKUP_PATH.exists():
logger.debug(f"删除旧的webui文件夹 {BACKUP_PATH}", COMMAND)
shutil.rmtree(BACKUP_PATH)
return "Webui更新成功!"
except Exception as e:
logger.error("更新webui失败...", COMMAND, e=e)
if BACKUP_PATH.exists():
logger.debug(f"恢复旧的webui文件夹 {BACKUP_PATH}", COMMAND)
BACKUP_PATH.rename(WEBUI_PATH)
raise e
return ""
@classmethod
async def check_version(cls) -> str:
"""检查更新版本
@@ -173,75 +19,142 @@ class UpdateManager:
str: 更新信息
"""
cur_version = cls.__get_version()
data = await cls.__get_latest_data()
if not data:
release_data = await ZhenxunRepoManager.zhenxun_get_latest_releases_data()
if not release_data:
return "检查更新获取版本失败..."
return (
"检测到当前版本更新\n"
f"当前版本:{cur_version}\n"
f"最新版本:{data.get('name')}\n"
f"创建日期:{data.get('created_at')}\n"
f"更新内容:\n{data.get('body')}"
f"最新版本:{release_data.get('name')}\n"
f"创建日期:{release_data.get('created_at')}\n"
f"更新内容:\n{release_data.get('body')}"
)
@classmethod
async def update(cls, bot: Bot, user_id: str, version_type: str) -> str:
async def update_webui(
cls,
source: Literal["git", "ali"] | None,
branch: str = "main",
force: bool = False,
):
"""更新WebUI
参数:
source: 更新源
branch: 分支
force: 是否强制更新
返回:
str: 返回消息
"""
if not source:
await ZhenxunRepoManager.webui_zip_update()
return "WebUI更新完成!"
result = await ZhenxunRepoManager.webui_git_update(
source,
branch=branch,
force=force,
)
if not result.success:
logger.error(f"WebUI更新失败...错误: {result.error_message}", LOG_COMMAND)
return f"WebUI更新失败...错误: {result.error_message}"
return "WebUI更新完成!"
@classmethod
async def update_resources(
cls,
source: Literal["git", "ali"] | None,
branch: str = "main",
force: bool = False,
) -> str:
"""更新资源
参数:
source: 更新源
branch: 分支
force: 是否强制更新
返回:
str: 返回消息
"""
if not source:
await ZhenxunRepoManager.resources_zip_update()
return "真寻资源更新完成!"
result = await ZhenxunRepoManager.resources_git_update(
source,
branch=branch,
force=force,
)
if not result.success:
logger.error(
f"真寻资源更新失败...错误: {result.error_message}", LOG_COMMAND
)
return f"真寻资源更新失败...错误: {result.error_message}"
return "真寻资源更新完成!"
@classmethod
async def update_zhenxun(
cls,
bot: Bot,
user_id: str,
version_type: Literal["main", "release"],
force: bool,
source: Literal["git", "ali"],
zip: bool,
) -> str:
"""更新操作
参数:
bot: Bot
user_id: 用户id
version_type: 更新版本类型
force: 是否强制更新
source: 更新源
zip: 是否下载zip文件
update_type: 更新方式
返回:
str | None: 返回消息
"""
logger.info("开始下载真寻最新版文件....", COMMAND)
cur_version = cls.__get_version()
url = None
new_version = None
repo_info = GithubUtils.parse_github_url(DEFAULT_GITHUB_URL)
if version_type in {"main"}:
repo_info.branch = version_type
new_version = await cls.__get_version_from_repo(repo_info)
if new_version:
new_version = new_version.split(":")[-1].strip()
url = await repo_info.get_archive_download_urls()
elif version_type == "release":
data = await cls.__get_latest_data()
if not data:
return "获取更新版本失败..."
new_version = data.get("name", "")
url = await repo_info.get_release_source_download_urls_tgz(new_version)
if not url:
return "获取版本下载链接失败..."
if TMP_PATH.exists():
logger.debug(f"删除临时文件夹 {TMP_PATH}", COMMAND)
shutil.rmtree(TMP_PATH)
logger.debug(
f"开始更新版本:{cur_version} -> {new_version} | 下载链接:{url}",
COMMAND,
)
await PlatformUtils.send_superuser(
bot,
f"检测真寻已更新,版本更新:{cur_version} -> {new_version}\n开始更新...",
f"检测真寻已更新,当前版本:{cur_version}\n开始更新...",
user_id,
)
download_file = (
DOWNLOAD_GZ_FILE if version_type == "release" else DOWNLOAD_ZIP_FILE
)
if await AsyncHttpx.download_file(url, download_file, stream=True):
logger.debug("下载真寻最新版文件完成...", COMMAND)
await _file_handle(new_version)
result = "版本更新完成"
if zip:
new_version = await ZhenxunRepoManager.zhenxun_zip_update(version_type)
await PlatformUtils.send_superuser(
bot, "真寻更新完成,开始安装依赖...", user_id
)
await VirtualEnvPackageManager.install_requirement(REQUIREMENTS_FILE)
return (
f"{result}\n"
f"版本: {cur_version} -> {new_version}\n"
f"版本更新完成!\n版本: {cur_version} -> {new_version}\n"
"请重新启动真寻以完成更新!"
)
else:
logger.debug("下载真寻最新版文件失败...", COMMAND)
return ""
result = await ZhenxunRepoManager.zhenxun_git_update(
source,
branch=version_type,
force=force,
)
if not result.success:
logger.error(
f"真寻版本更新失败...错误: {result.error_message}",
LOG_COMMAND,
)
return f"版本更新失败...错误: {result.error_message}"
await PlatformUtils.send_superuser(
bot, "真寻更新完成,开始安装依赖...", user_id
)
await VirtualEnvPackageManager.install_requirement(REQUIREMENTS_FILE)
return (
f"版本更新完成!\n"
f"版本: {cur_version} -> {result.new_version}\n"
f"变更文件个数: {len(result.changed_files)}"
f"{'' if source == 'git' else '(阿里云更新不支持查看变更文件)'}\n"
"请重新启动真寻以完成更新!"
)
@classmethod
def __get_version(cls) -> str:
@@ -255,40 +168,3 @@ class UpdateManager:
if text := VERSION_FILE.open(encoding="utf8").readline():
_version = text.split(":")[-1].strip()
return _version
@classmethod
async def __get_latest_data(cls) -> dict:
"""获取最新版本信息
返回:
dict: 最新版本数据
"""
for _ in range(3):
try:
res = await AsyncHttpx.get(RELEASE_URL)
if res.status_code == 200:
return res.json()
except TimeoutError:
pass
except Exception as e:
logger.error("检查更新真寻获取版本失败", e=e)
return {}
@classmethod
async def __get_version_from_repo(cls, repo_info: RepoInfo) -> str:
"""从指定分支获取版本号
参数:
branch: 分支名称
返回:
str: 版本号
"""
version_url = await repo_info.get_raw_download_urls(path="__version__")
try:
res = await AsyncHttpx.get(version_url)
if res.status_code == 200:
return res.text.strip()
except Exception as e:
logger.error(f"获取 {repo_info.branch} 分支版本失败", e=e)
return "未知版本"
+3 -34
View File
@@ -1,38 +1,7 @@
from pathlib import Path
from zhenxun.configs.path_config import TEMP_PATH
LOG_COMMAND = "AutoUpdate"
DEFAULT_GITHUB_URL = "https://github.com/HibiKier/zhenxun_bot/tree/main"
RELEASE_URL = "https://api.github.com/repos/HibiKier/zhenxun_bot/releases/latest"
VERSION_FILE = Path() / "__version__"
VERSION_FILE_STRING = "__version__"
VERSION_FILE = Path() / VERSION_FILE_STRING
PYPROJECT_FILE_STRING = "pyproject.toml"
PYPROJECT_FILE = Path() / PYPROJECT_FILE_STRING
PYPROJECT_LOCK_FILE_STRING = "poetry.lock"
PYPROJECT_LOCK_FILE = Path() / PYPROJECT_LOCK_FILE_STRING
REQ_TXT_FILE_STRING = "requirements.txt"
REQ_TXT_FILE = Path() / REQ_TXT_FILE_STRING
BASE_PATH_STRING = "zhenxun"
BASE_PATH = Path() / BASE_PATH_STRING
TMP_PATH = TEMP_PATH / "auto_update"
BACKUP_PATH = Path() / "backup"
DOWNLOAD_GZ_FILE_STRING = "download_latest_file.tar.gz"
DOWNLOAD_ZIP_FILE_STRING = "download_latest_file.zip"
DOWNLOAD_GZ_FILE = TMP_PATH / DOWNLOAD_GZ_FILE_STRING
DOWNLOAD_ZIP_FILE = TMP_PATH / DOWNLOAD_ZIP_FILE_STRING
REPLACE_FOLDERS = [
"builtin_plugins",
"services",
"utils",
"models",
"configs",
]
COMMAND = "检查更新"
REQUIREMENTS_FILE = Path() / "requirements.txt"
@@ -148,6 +148,11 @@ async def get_plugin_and_user(
user = await with_timeout(
user_dao.safe_get_or_none(user_id=user_id), name="get_user"
)
except IntegrityError:
await asyncio.sleep(0.5)
plugin, user = await with_timeout(
asyncio.gather(plugin_task, user_task), name="get_plugin_and_user"
)
if not plugin:
raise PermissionExemption(f"插件:{module} 数据不存在,已跳过权限检查...")
@@ -84,7 +84,7 @@ async def _(session: EventSession):
try:
result = await StoreManager.get_plugins_info()
logger.info("查看插件列表", "插件商店", session=session)
await MessageUtils.build_message(result).send()
await MessageUtils.build_message([*result]).send()
except Exception as e:
logger.error(f"查看插件列表失败 e: {e}", "插件商店", session=session, e=e)
await MessageUtils.build_message("获取插件列表失败...").send()
@@ -1,19 +1,19 @@
from pathlib import Path
import random
import shutil
from aiocache import cached
import ujson as json
from zhenxun.builtin_plugins.auto_update.config import REQ_TXT_FILE_STRING
from zhenxun.builtin_plugins.plugin_store.models import StorePluginInfo
from zhenxun.configs.path_config import TEMP_PATH
from zhenxun.models.plugin_info import PluginInfo
from zhenxun.services.log import logger
from zhenxun.services.plugin_init import PluginInitManager
from zhenxun.utils.github_utils import GithubUtils
from zhenxun.utils.github_utils.models import RepoAPI
from zhenxun.utils.http_utils import AsyncHttpx
from zhenxun.utils.image_utils import BuildImage, ImageTemplate, RowStyle
from zhenxun.utils.manager.virtual_env_package_manager import VirtualEnvPackageManager
from zhenxun.utils.repo_utils import RepoFileManager
from zhenxun.utils.repo_utils.models import RepoFileInfo, RepoType
from zhenxun.utils.utils import is_number
from .config import (
@@ -22,6 +22,7 @@ from .config import (
EXTRA_GITHUB_URL,
LOG_COMMAND,
)
from .exceptions import PluginStoreException
def row_style(column: str, text: str) -> RowStyle:
@@ -40,73 +41,25 @@ def row_style(column: str, text: str) -> RowStyle:
return style
def install_requirement(plugin_path: Path):
requirement_files = ["requirement.txt", "requirements.txt"]
requirement_paths = [plugin_path / file for file in requirement_files]
if existing_requirements := next(
(path for path in requirement_paths if path.exists()), None
):
VirtualEnvPackageManager.install_requirement(existing_requirements)
class StoreManager:
@classmethod
async def get_github_plugins(cls) -> list[StorePluginInfo]:
"""获取github插件列表信息
返回:
list[StorePluginInfo]: 插件列表数据
"""
repo_info = GithubUtils.parse_github_url(DEFAULT_GITHUB_URL)
if await repo_info.update_repo_commit():
logger.info(f"获取最新提交: {repo_info.branch}", LOG_COMMAND)
else:
logger.warning(f"获取最新提交失败: {repo_info}", LOG_COMMAND)
default_github_url = await repo_info.get_raw_download_urls("plugins.json")
response = await AsyncHttpx.get(default_github_url, check_status_code=200)
if response.status_code == 200:
logger.info("获取github插件列表成功", LOG_COMMAND)
return [StorePluginInfo(**detail) for detail in json.loads(response.text)]
else:
logger.warning(
f"获取github插件列表失败: {response.status_code}", LOG_COMMAND
)
return []
@classmethod
async def get_extra_plugins(cls) -> list[StorePluginInfo]:
"""获取额外插件列表信息
返回:
list[StorePluginInfo]: 插件列表数据
"""
repo_info = GithubUtils.parse_github_url(EXTRA_GITHUB_URL)
if await repo_info.update_repo_commit():
logger.info(f"获取最新提交: {repo_info.branch}", LOG_COMMAND)
else:
logger.warning(f"获取最新提交失败: {repo_info}", LOG_COMMAND)
extra_github_url = await repo_info.get_raw_download_urls("plugins.json")
response = await AsyncHttpx.get(extra_github_url, check_status_code=200)
if response.status_code == 200:
return [StorePluginInfo(**detail) for detail in json.loads(response.text)]
else:
logger.warning(
f"获取github扩展插件列表失败: {response.status_code}", LOG_COMMAND
)
return []
@classmethod
@cached(60)
async def get_data(cls) -> list[StorePluginInfo]:
async def get_data(cls) -> tuple[list[StorePluginInfo], list[StorePluginInfo]]:
"""获取插件信息数据
返回:
list[StorePluginInfo]: 插件信息数据
tuple[list[StorePluginInfo], list[StorePluginInfo]]:
原生插件信息数据,第三方插件信息数据
"""
plugins = await cls.get_github_plugins()
extra_plugins = await cls.get_extra_plugins()
return [*plugins, *extra_plugins]
plugins = await RepoFileManager.get_file_content(
DEFAULT_GITHUB_URL, "plugins.json"
)
extra_plugins = await RepoFileManager.get_file_content(
EXTRA_GITHUB_URL, "plugins.json", "index"
)
return [StorePluginInfo(**plugin) for plugin in json.loads(plugins)], [
StorePluginInfo(**plugin) for plugin in json.loads(extra_plugins)
]
@classmethod
def version_check(cls, plugin_info: StorePluginInfo, suc_plugin: dict[str, str]):
@@ -152,38 +105,105 @@ class StoreManager:
return await PluginInfo.filter(load_status=True).values_list(*args)
@classmethod
async def get_plugins_info(cls) -> BuildImage | str:
async def get_plugins_info(cls) -> list[BuildImage] | str:
"""插件列表
返回:
BuildImage | str: 返回消息
"""
plugin_list: list[StorePluginInfo] = await cls.get_data()
plugin_list, extra_plugin_list = await cls.get_data()
column_name = ["-", "ID", "名称", "简介", "作者", "版本", "类型"]
db_plugin_list = await cls.get_loaded_plugins("module", "version")
suc_plugin = {p[0]: (p[1] or "0.1") for p in db_plugin_list}
data_list = [
[
"已安装" if plugin_info.module in suc_plugin else "",
id,
plugin_info.name,
plugin_info.description,
plugin_info.author,
cls.version_check(plugin_info, suc_plugin),
plugin_info.plugin_type_name,
]
for id, plugin_info in enumerate(plugin_list)
index = 0
data_list = []
extra_data_list = []
for plugin_info in plugin_list:
data_list.append(
[
"已安装" if plugin_info.module in suc_plugin else "",
index,
plugin_info.name,
plugin_info.description,
plugin_info.author,
cls.version_check(plugin_info, suc_plugin),
plugin_info.plugin_type_name,
]
)
index += 1
for plugin_info in extra_plugin_list:
extra_data_list.append(
[
"已安装" if plugin_info.module in suc_plugin else "",
index,
plugin_info.name,
plugin_info.description,
plugin_info.author,
cls.version_check(plugin_info, suc_plugin),
plugin_info.plugin_type_name,
]
)
index += 1
return [
await ImageTemplate.table_page(
"原生插件列表",
"通过添加/移除插件 ID 来管理插件",
column_name,
data_list,
text_style=row_style,
),
await ImageTemplate.table_page(
"第三方插件列表",
"通过添加/移除插件 ID 来管理插件",
column_name,
extra_data_list,
text_style=row_style,
),
]
return await ImageTemplate.table_page(
"插件列表",
"通过添加/移除插件 ID 来管理插件",
column_name,
data_list,
text_style=row_style,
)
@classmethod
async def add_plugin(cls, plugin_id: str) -> str:
async def get_plugin_by_value(
cls, index_or_module: str, is_update: bool = False
) -> tuple[StorePluginInfo, bool]:
"""获取插件信息
参数:
index_or_module: 插件索引或模块名
is_update: 是否是更新插件
异常:
PluginStoreException: 插件不存在
PluginStoreException: 插件已安装
返回:
StorePluginInfo: 插件信息
bool: 是否是外部插件
"""
plugin_list, extra_plugin_list = await cls.get_data()
plugin_info = None
is_external = False
db_plugin_list = await cls.get_loaded_plugins("module")
plugin_key = await cls._resolve_plugin_key(index_or_module)
for p in plugin_list:
if p.module == plugin_key:
is_external = False
plugin_info = p
break
for p in extra_plugin_list:
if p.module == plugin_key:
is_external = True
plugin_info = p
break
if not plugin_info:
raise PluginStoreException(f"插件不存在: {plugin_key}")
if not is_update and plugin_info.module in [p[0] for p in db_plugin_list]:
raise PluginStoreException(f"插件 {plugin_info.name} 已安装,无需重复安装")
if plugin_info.module not in [p[0] for p in db_plugin_list] and is_update:
raise PluginStoreException(f"插件 {plugin_info.name} 未安装,无法更新")
return plugin_info, is_external
@classmethod
async def add_plugin(cls, index_or_module: str) -> str:
"""添加插件
参数:
@@ -192,21 +212,9 @@ class StoreManager:
返回:
str: 返回消息
"""
plugin_list: list[StorePluginInfo] = await cls.get_data()
try:
plugin_key = await cls._resolve_plugin_key(plugin_id)
except ValueError as e:
return str(e)
db_plugin_list = await cls.get_loaded_plugins("module")
plugin_info = next((p for p in plugin_list if p.module == plugin_key), None)
if plugin_info is None:
return f"未找到插件 {plugin_key}"
if plugin_info.module in [p[0] for p in db_plugin_list]:
return f"插件 {plugin_info.name} 已安装,无需重复安装"
is_external = True
plugin_info, is_external = await cls.get_plugin_by_value(index_or_module)
if plugin_info.github_url is None:
plugin_info.github_url = DEFAULT_GITHUB_URL
is_external = False
version_split = plugin_info.version.split("-")
if len(version_split) > 1:
github_url_split = plugin_info.github_url.split("/tree/")
@@ -228,90 +236,81 @@ class StoreManager:
is_dir: bool,
is_external: bool = False,
):
repo_api: RepoAPI
repo_info = GithubUtils.parse_github_url(github_url)
if await repo_info.update_repo_commit():
logger.info(f"获取最新提交: {repo_info.branch}", LOG_COMMAND)
else:
logger.warning(f"获取最新提交失败: {repo_info}", LOG_COMMAND)
logger.debug(f"成功获取仓库信息: {repo_info}", LOG_COMMAND)
for repo_api in GithubUtils.iter_api_strategies():
try:
await repo_api.parse_repo_info(repo_info)
break
except Exception as e:
logger.warning(
f"获取插件文件失败 | API类型: {repo_api.strategy}",
LOG_COMMAND,
e=e,
)
continue
else:
raise ValueError("所有API获取插件文件失败,请检查网络连接")
if module_path == ".":
module_path = ""
"""安装插件
参数:
github_url: 仓库地址
module_path: 模块路径
is_dir: 是否是文件夹
is_external: 是否是外部仓库
"""
repo_type = RepoType.GITHUB if is_external else None
replace_module_path = module_path.replace(".", "/")
files = repo_api.get_files(
module_path=replace_module_path + ("" if is_dir else ".py"),
is_dir=is_dir,
)
download_urls = [await repo_info.get_raw_download_urls(file) for file in files]
base_path = BASE_PATH / "plugins" if is_external else BASE_PATH
base_path = base_path if module_path else base_path / repo_info.repo
download_paths: list[Path | str] = [base_path / file for file in files]
logger.debug(f"插件下载路径: {download_paths}", LOG_COMMAND)
result = await AsyncHttpx.gather_download_file(download_urls, download_paths)
for _id, success in enumerate(result):
if not success:
break
if is_dir:
files = await RepoFileManager.list_directory_files(
github_url, replace_module_path, repo_type=repo_type
)
else:
# 安装依赖
plugin_path = base_path / "/".join(module_path.split("."))
try:
req_files = repo_api.get_files(
f"{replace_module_path}/{REQ_TXT_FILE_STRING}", False
files = [RepoFileInfo(path=f"{replace_module_path}.py", is_dir=False)]
local_path = BASE_PATH / "plugins" if is_external else BASE_PATH
files = [file for file in files if not file.is_dir]
download_files = [(file.path, local_path / file.path) for file in files]
await RepoFileManager.download_files(
github_url, download_files, repo_type=repo_type
)
requirement_paths = [
file
for file in files
if file.path.endswith("requirement.txt")
or file.path.endswith("requirements.txt")
]
is_install_req = False
for requirement_path in requirement_paths:
requirement_file = local_path / requirement_path.path
if requirement_file.exists():
is_install_req = True
await VirtualEnvPackageManager.install_requirement(requirement_file)
if not is_install_req:
# 从仓库根目录查找文件
rand = random.randint(1, 10000)
requirement_path = TEMP_PATH / f"plugin_store_{rand}_req.txt"
requirements_path = TEMP_PATH / f"plugin_store_{rand}_reqs.txt"
await RepoFileManager.download_files(
github_url,
[
("requirement.txt", requirement_path),
("requirements.txt", requirements_path),
],
repo_type=repo_type,
ignore_error=True,
)
if requirement_path.exists():
logger.info(
f"开始安装插件 {module_path} 依赖文件: {requirement_path}",
LOG_COMMAND,
)
req_files.extend(
repo_api.get_files(f"{replace_module_path}/requirement.txt", False)
await VirtualEnvPackageManager.install_requirement(requirement_path)
if requirements_path.exists():
logger.info(
f"开始安装插件 {module_path} 依赖文件: {requirements_path}",
LOG_COMMAND,
)
logger.debug(f"获取插件依赖文件列表: {req_files}", LOG_COMMAND)
req_download_urls = [
await repo_info.get_raw_download_urls(file) for file in req_files
]
req_paths: list[Path | str] = [plugin_path / file for file in req_files]
logger.debug(f"插件依赖文件下载路径: {req_paths}", LOG_COMMAND)
if req_files:
result = await AsyncHttpx.gather_download_file(
req_download_urls, req_paths
)
for success in result:
if not success:
raise Exception("插件依赖文件下载失败")
logger.debug(f"插件依赖文件列表: {req_paths}", LOG_COMMAND)
install_requirement(plugin_path)
except ValueError as e:
logger.warning("未获取到依赖文件路径...", e=e)
return True
raise Exception("插件下载失败...")
await VirtualEnvPackageManager.install_requirement(requirements_path)
@classmethod
async def remove_plugin(cls, plugin_id: str) -> str:
async def remove_plugin(cls, index_or_module: str) -> str:
"""移除插件
参数:
plugin_id: 插件id或模块名
index_or_module: 插件id或模块名
返回:
str: 返回消息
"""
plugin_list: list[StorePluginInfo] = await cls.get_data()
try:
plugin_key = await cls._resolve_plugin_key(plugin_id)
except ValueError as e:
return str(e)
plugin_info = next((p for p in plugin_list if p.module == plugin_key), None)
if plugin_info is None:
return f"未找到插件 {plugin_key}"
plugin_info, _ = await cls.get_plugin_by_value(index_or_module)
path = BASE_PATH
if plugin_info.github_url:
path = BASE_PATH / "plugins"
@@ -339,12 +338,13 @@ class StoreManager:
返回:
BuildImage | str: 返回消息
"""
plugin_list: list[StorePluginInfo] = await cls.get_data()
plugin_list, extra_plugin_list = await cls.get_data()
all_plugin_list = plugin_list + extra_plugin_list
db_plugin_list = await cls.get_loaded_plugins("module", "version")
suc_plugin = {p[0]: (p[1] or "Unknown") for p in db_plugin_list}
filtered_data = [
(id, plugin_info)
for id, plugin_info in enumerate(plugin_list)
for id, plugin_info in enumerate(all_plugin_list)
if plugin_name_or_author.lower() in plugin_info.name.lower()
or plugin_name_or_author.lower() in plugin_info.author.lower()
]
@@ -373,35 +373,24 @@ class StoreManager:
)
@classmethod
async def update_plugin(cls, plugin_id: str) -> str:
async def update_plugin(cls, index_or_module: str) -> str:
"""更新插件
参数:
plugin_id: 插件id
index_or_module: 插件id
返回:
str: 返回消息
"""
plugin_list: list[StorePluginInfo] = await cls.get_data()
try:
plugin_key = await cls._resolve_plugin_key(plugin_id)
except ValueError as e:
return str(e)
plugin_info = next((p for p in plugin_list if p.module == plugin_key), None)
if plugin_info is None:
return f"未找到插件 {plugin_key}"
plugin_info, is_external = await cls.get_plugin_by_value(index_or_module, True)
logger.info(f"尝试更新插件 {plugin_info.name}", LOG_COMMAND)
db_plugin_list = await cls.get_loaded_plugins("module", "version")
suc_plugin = {p[0]: (p[1] or "Unknown") for p in db_plugin_list}
if plugin_info.module not in [p[0] for p in db_plugin_list]:
return f"插件 {plugin_info.name} 未安装,无法更新"
logger.debug(f"当前插件列表: {suc_plugin}", LOG_COMMAND)
if cls.check_version_is_new(plugin_info, suc_plugin):
return f"插件 {plugin_info.name} 已是最新版本"
is_external = True
if plugin_info.github_url is None:
plugin_info.github_url = DEFAULT_GITHUB_URL
is_external = False
await cls.install_plugin_with_repo(
plugin_info.github_url,
plugin_info.module_path,
@@ -420,8 +409,9 @@ class StoreManager:
返回:
str: 返回消息
"""
plugin_list: list[StorePluginInfo] = await cls.get_data()
plugin_name_list = [p.name for p in plugin_list]
plugin_list, extra_plugin_list = await cls.get_data()
all_plugin_list = plugin_list + extra_plugin_list
plugin_name_list = [p.name for p in all_plugin_list]
update_failed_list = []
update_success_list = []
result = "--已更新{}个插件 {}个失败 {}个成功--"
@@ -492,22 +482,25 @@ class StoreManager:
plugin_id: module,id或插件名称
异常:
ValueError: 插件不存在
ValueError: 插件不存在
PluginStoreException: 插件不存在
PluginStoreException: 插件不存在
返回:
str: 插件模块名
"""
plugin_list: list[StorePluginInfo] = await cls.get_data()
plugin_list, extra_plugin_list = await cls.get_data()
all_plugin_list = plugin_list + extra_plugin_list
if is_number(plugin_id):
idx = int(plugin_id)
if idx < 0 or idx >= len(plugin_list):
raise ValueError("插件ID不存在...")
return plugin_list[idx].module
if idx < 0 or idx >= len(all_plugin_list):
raise PluginStoreException("插件ID不存在...")
return all_plugin_list[idx].module
elif isinstance(plugin_id, str):
result = (
None if plugin_id not in [v.module for v in plugin_list] else plugin_id
) or next(v for v in plugin_list if v.name == plugin_id).module
None
if plugin_id not in [v.module for v in all_plugin_list]
else plugin_id
) or next(v for v in all_plugin_list if v.name == plugin_id).module
if not result:
raise ValueError("插件 Module / 名称 不存在...")
raise PluginStoreException("插件 Module / 名称 不存在...")
return result
@@ -0,0 +1,6 @@
class PluginStoreException(Exception):
def __init__(self, message: str):
self.message = message
def __str__(self):
return self.message
@@ -38,10 +38,11 @@ async def _(setting: Setting) -> Result:
password = Config.get_config("web-ui", "password")
if password or BotConfig.db_url:
return Result.fail("配置已存在,请先删除DB_URL内容和前端密码再进行设置。")
env_file = Path() / ".env.dev"
env_file = Path() / ".env.example"
if not env_file.exists():
return Result.fail("配置文件.env.dev不存在。")
return Result.fail("基础配置文件.env.example不存在。")
env_text = env_file.read_text(encoding="utf-8")
to_env_file = Path() / ".env.dev"
if setting.db_url:
if setting.db_url.startswith("sqlite"):
base_dir = Path().resolve()
@@ -78,7 +79,7 @@ async def _(setting: Setting) -> Result:
if setting.username:
Config.set_config("web-ui", "username", setting.username)
Config.set_config("web-ui", "password", setting.password, True)
env_file.write_text(env_text, encoding="utf-8")
to_env_file.write_text(env_text, encoding="utf-8")
if BAT_FILE.exists():
for file in os.listdir(Path()):
if file.startswith(FILE_NAME):
@@ -229,9 +229,9 @@ async def _(payload: InstallDependenciesPayload) -> Result:
if not payload.dependencies:
return Result.fail("依赖列表不能为空")
if payload.handle_type == "install":
result = VirtualEnvPackageManager.install(payload.dependencies)
result = await VirtualEnvPackageManager.install(payload.dependencies)
else:
result = VirtualEnvPackageManager.uninstall(payload.dependencies)
result = await VirtualEnvPackageManager.uninstall(payload.dependencies)
return Result.ok(result)
except Exception as e:
logger.error(f"{router.prefix}/install_dependencies 调用错误", "WebUi", e=e)
@@ -25,10 +25,10 @@ async def _() -> Result[dict]:
require("plugin_store")
from zhenxun.builtin_plugins.plugin_store import StoreManager
data = await StoreManager.get_data()
plugin_list, extra_plugin_list = await StoreManager.get_data()
plugin_list = [
{**model_dump(plugin), "name": plugin.name, "id": idx}
for idx, plugin in enumerate(data)
for idx, plugin in enumerate(plugin_list + extra_plugin_list)
]
modules = await PluginInfo.filter(load_status=True).values_list(
"module", flat=True