diff --git a/zhenxun/builtin_plugins/auto_update/__init__.py b/zhenxun/builtin_plugins/auto_update/__init__.py index 32040400..19af0fa3 100644 --- a/zhenxun/builtin_plugins/auto_update/__init__.py +++ b/zhenxun/builtin_plugins/auto_update/__init__.py @@ -84,13 +84,16 @@ async def _( ): result = "" await MessageUtils.build_message("正在进行检查更新...").send(reply_to=True) + + if not ver_type.available: + result += await UpdateManager.check_version() + logger.info("查看当前版本...", "检查更新", session=session) + await MessageUtils.build_message(result).finish() + return + 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() - logger.info("查看当前版本...", "检查更新", session=session) - await MessageUtils.build_message(result).finish() try: result += await UpdateManager.update_zhenxun( bot, diff --git a/zhenxun/builtin_plugins/auto_update/_data_source.py b/zhenxun/builtin_plugins/auto_update/_data_source.py index b9572d78..9b41d596 100644 --- a/zhenxun/builtin_plugins/auto_update/_data_source.py +++ b/zhenxun/builtin_plugins/auto_update/_data_source.py @@ -1,6 +1,9 @@ +import asyncio from typing import Literal from nonebot.adapters import Bot +from packaging.specifiers import SpecifierSet +from packaging.version import InvalidVersion, Version from zhenxun.services.log import logger from zhenxun.utils.manager.virtual_env_package_manager import VirtualEnvPackageManager @@ -9,6 +12,7 @@ from zhenxun.utils.manager.zhenxun_repo_manager import ( ZhenxunRepoManager, ) from zhenxun.utils.platform import PlatformUtils +from zhenxun.utils.repo_utils import RepoFileManager LOG_COMMAND = "AutoUpdate" @@ -16,22 +20,101 @@ LOG_COMMAND = "AutoUpdate" class UpdateManager: @classmethod async def check_version(cls) -> str: - """检查更新版本 + """检查真寻和资源的版本""" + bot_cur_version = cls.__get_version() - 返回: - str: 更新信息 - """ - cur_version = cls.__get_version() - release_data = await ZhenxunRepoManager.zhenxun_get_latest_releases_data() - if not release_data: - return "检查更新获取版本失败..." - return ( - "检测到当前版本更新\n" - f"当前版本:{cur_version}\n" - f"最新版本:{release_data.get('name')}\n" - f"创建日期:{release_data.get('created_at')}\n" - f"更新内容:\n{release_data.get('body')}" + release_task = ZhenxunRepoManager.zhenxun_get_latest_releases_data() + dev_version_task = RepoFileManager.get_file_content( + ZhenxunRepoConfig.ZHENXUN_BOT_GITHUB_URL, "__version__" ) + bot_commit_date_task = RepoFileManager.get_file_last_commit_date( + ZhenxunRepoConfig.ZHENXUN_BOT_GITHUB_URL, "__version__" + ) + res_commit_date_task = RepoFileManager.get_file_last_commit_date( + ZhenxunRepoConfig.RESOURCE_GITHUB_URL, "__version__" + ) + + ( + release_data, + dev_version_text, + bot_commit_date, + res_commit_date, + ) = await asyncio.gather( + release_task, + dev_version_task, + bot_commit_date_task, + res_commit_date_task, + return_exceptions=True, + ) + + if isinstance(release_data, dict): + bot_release_version = release_data.get("name", "获取失败") + bot_release_date = release_data.get("created_at", "").split("T")[0] + else: + bot_release_version = "获取失败" + bot_release_date = "获取失败" + logger.warning(f"获取 Bot release 信息失败: {release_data}") + + if isinstance(dev_version_text, str): + bot_dev_version = dev_version_text.split(":")[-1].strip() + else: + bot_dev_version = "获取失败" + bot_commit_date = "获取失败" + logger.warning(f"获取 Bot dev 版本信息失败: {dev_version_text}") + + bot_update_hint = "" + try: + cur_base_v = bot_cur_version.split("-")[0].lstrip("v") + dev_base_v = bot_dev_version.split("-")[0].lstrip("v") + + if Version(cur_base_v) < Version(dev_base_v): + bot_update_hint = "\n-> 发现新开发版本, 可用 `检查更新 main` 更新" + elif ( + Version(cur_base_v) == Version(dev_base_v) + and bot_cur_version != bot_dev_version + ): + bot_update_hint = "\n-> 发现新开发版本, 可用 `检查更新 main` 更新" + except (InvalidVersion, TypeError, IndexError): + if bot_cur_version != bot_dev_version and bot_dev_version != "获取失败": + bot_update_hint = "\n-> 发现新开发版本, 可用 `检查更新 main` 更新" + + bot_update_info = ( + f"当前版本: {bot_cur_version}\n" + f"最新开发版: {bot_dev_version} (更新于: {bot_commit_date})\n" + f"最新正式版: {bot_release_version} (发布于: {bot_release_date})" + f"{bot_update_hint}" + ) + + res_version_file = ZhenxunRepoConfig.RESOURCE_PATH / "__version__" + res_cur_version = "未找到" + if res_version_file.exists(): + if text := res_version_file.open(encoding="utf8").readline(): + res_cur_version = text.split(":")[-1].strip() + + res_latest_version = "获取失败" + try: + res_latest_version_text = await RepoFileManager.get_file_content( + ZhenxunRepoConfig.RESOURCE_GITHUB_URL, "__version__" + ) + res_latest_version = res_latest_version_text.split(":")[-1].strip() + except Exception as e: + res_commit_date = "获取失败" + logger.warning(f"获取资源版本信息失败: {e}") + + res_update_hint = "" + try: + if Version(res_cur_version) < Version(res_latest_version): + res_update_hint = "\n-> 发现新资源版本, 可用 `检查更新 resource` 更新" + except (InvalidVersion, TypeError): + pass + + res_update_info = ( + f"当前版本: {res_cur_version}\n" + f"最新版本: {res_latest_version} (更新于: {res_commit_date})" + f"{res_update_hint}" + ) + + return f"『绪山真寻 Bot』\n{bot_update_info}\n\n『真寻资源』\n{res_update_info}" @classmethod async def update_webui( @@ -125,6 +208,7 @@ class UpdateManager: f"检测真寻已更新,当前版本:{cur_version}\n开始更新...", user_id, ) + result_message = "" if zip: new_version = await ZhenxunRepoManager.zhenxun_zip_update(version_type) await PlatformUtils.send_superuser( @@ -133,7 +217,7 @@ class UpdateManager: await VirtualEnvPackageManager.install_requirement( ZhenxunRepoConfig.REQUIREMENTS_FILE ) - return ( + result_message = ( f"版本更新完成!\n版本: {cur_version} -> {new_version}\n" "请重新启动真寻以完成更新!" ) @@ -155,13 +239,54 @@ class UpdateManager: await VirtualEnvPackageManager.install_requirement( ZhenxunRepoConfig.REQUIREMENTS_FILE ) - return ( + result_message = ( f"版本更新完成!\n" f"版本: {cur_version} -> {result.new_version}\n" f"变更文件个数: {len(result.changed_files)}" f"{'' if source == 'git' else '(阿里云更新不支持查看变更文件)'}\n" "请重新启动真寻以完成更新!" ) + resource_warning = "" + if version_type == "main": + try: + spec_content = await RepoFileManager.get_file_content( + ZhenxunRepoConfig.ZHENXUN_BOT_GITHUB_URL, "resources.spec" + ) + required_spec_str = None + for line in spec_content.splitlines(): + if line.startswith("require_resources_version:"): + required_spec_str = line.split(":", 1)[1].strip().strip("\"'") + break + if required_spec_str: + res_version_file = ZhenxunRepoConfig.RESOURCE_PATH / "__version__" + local_res_version_str = "0.0.0" + if res_version_file.exists(): + if text := res_version_file.open(encoding="utf8").readline(): + local_res_version_str = text.split(":")[-1].strip() + + spec = SpecifierSet(required_spec_str) + local_ver = Version(local_res_version_str) + if not spec.contains(local_ver): + warning_header = ( + f"⚠️ **资源版本不兼容!**\n" + f"当前代码需要资源版本: `{required_spec_str}`\n" + f"您当前的资源版本是: `{local_res_version_str}`\n" + "**将自动为您更新资源文件...**" + ) + await PlatformUtils.send_superuser(bot, warning_header, user_id) + resource_update_source = None if zip else source + resource_update_result = await cls.update_resources( + source=resource_update_source, force=force + ) + resource_warning = ( + f"\n\n{warning_header}\n{resource_update_result}" + ) + except Exception as e: + logger.warning(f"检查资源版本兼容性时出错: {e}", LOG_COMMAND, e=e) + resource_warning = ( + "\n\n⚠️ 检查资源版本兼容性时出错,建议手动运行 `检查更新 resource`" + ) + return result_message + resource_warning @classmethod def __get_version(cls) -> str: diff --git a/zhenxun/utils/github_utils/const.py b/zhenxun/utils/github_utils/const.py index 102e6f19..e83f351d 100644 --- a/zhenxun/utils/github_utils/const.py +++ b/zhenxun/utils/github_utils/const.py @@ -40,6 +40,9 @@ RELEASE_SOURCE_FORMAT = ( GIT_API_COMMIT_FORMAT = "https://api.github.com/repos/{owner}/{repo}/commits/{branch}" """git api commit地址格式""" +GIT_API_COMMIT_LIST_FORMAT = "https://api.github.com/repos/{owner}/{repo}/commits" +"""git api 列出commits的地址格式""" + GIT_API_PROXY_COMMIT_FORMAT = ( "https://git-api.zhenxun.org/repos/{owner}/{repo}/commits/{branch}" ) diff --git a/zhenxun/utils/repo_utils/aliyun_manager.py b/zhenxun/utils/repo_utils/aliyun_manager.py index 302a24de..2f76003a 100644 --- a/zhenxun/utils/repo_utils/aliyun_manager.py +++ b/zhenxun/utils/repo_utils/aliyun_manager.py @@ -348,6 +348,11 @@ class AliyunCodeupManager(BaseRepoManager): if not self.config.aliyun_codeup.organization_id: raise AuthenticationError("阿里云CodeUp") + async def get_latest_commit(self, repo_url: str, branch: str = "main") -> str: + """获取阿里云CodeUp仓库指定分支的最新提交哈希值。""" + repo_name = repo_url.split("/tree/")[0].split("/")[-1].replace(".git", "") + return await self._get_newest_commit(repo_name, branch) + async def _get_newest_commit(self, repo_name: str, branch: str) -> str: """ 获取仓库最新提交ID diff --git a/zhenxun/utils/repo_utils/base_manager.py b/zhenxun/utils/repo_utils/base_manager.py index efe306b6..dd7d248e 100644 --- a/zhenxun/utils/repo_utils/base_manager.py +++ b/zhenxun/utils/repo_utils/base_manager.py @@ -117,6 +117,20 @@ class BaseRepoManager(ABC): """ pass + @abstractmethod + async def get_latest_commit(self, repo_url: str, branch: str = "main") -> str: + """ + 获取仓库指定分支的最新提交哈希值。 + + 参数: + repo_url: 仓库URL或名称。 + branch: 分支名称。 + + 返回: + str: 最新的提交哈希值。 + """ + pass + async def save_file_content(self, content: bytes, local_path: Path) -> int: """ 保存文件内容 diff --git a/zhenxun/utils/repo_utils/file_manager.py b/zhenxun/utils/repo_utils/file_manager.py index 94d50db3..7c6d2a0c 100644 --- a/zhenxun/utils/repo_utils/file_manager.py +++ b/zhenxun/utils/repo_utils/file_manager.py @@ -11,6 +11,7 @@ from httpx import Response from zhenxun.services.log import logger from zhenxun.utils.github_utils import GithubUtils +from zhenxun.utils.github_utils.const import GIT_API_COMMIT_LIST_FORMAT from zhenxun.utils.github_utils.models import AliyunTreeType, GitHubStrategy, TreeType from zhenxun.utils.http_utils import AsyncHttpx from zhenxun.utils.utils import is_binary_file @@ -633,3 +634,38 @@ class RepoFileManager: result.success = False result.error_message = str(e) return result + + async def get_file_last_commit_date( + self, repo_url: str, file_path: str + ) -> str | None: + """ + 获取 GitHub 仓库中指定文件的最新提交日期。 + + 参数: + repo_url: 仓库的URL。 + file_path: 文件在仓库中的路径。 + + 返回: + str | None: "YYYY-MM-DD" 格式的日期字符串,如果失败则返回 None。 + """ + try: + repo_info = GithubUtils.parse_github_url(repo_url) + api_url = GIT_API_COMMIT_LIST_FORMAT.format( + owner=repo_info.owner, repo=repo_info.repo + ) + params = { + "sha": repo_info.branch, + "path": file_path, + "page": 1, + "per_page": 1, + } + + data = await AsyncHttpx.get_json(api_url, params=params) + if data and isinstance(data, list) and data[0]: + date_str = data[0]["commit"]["committer"]["date"] + return date_str.split("T")[0] + except Exception as e: + logger.warning( + f"获取 {repo_url} 中 {file_path} 的 commit 日期失败", LOG_COMMAND, e=e + ) + return None diff --git a/zhenxun/utils/repo_utils/github_manager.py b/zhenxun/utils/repo_utils/github_manager.py index 462c2723..8acaf82d 100644 --- a/zhenxun/utils/repo_utils/github_manager.py +++ b/zhenxun/utils/repo_utils/github_manager.py @@ -320,6 +320,12 @@ class GithubManager(BaseRepoManager): logger.error("获取提交信息失败", LOG_COMMAND, e=e) return None + async def get_latest_commit(self, repo_url: str, branch: str = "main") -> str: + """获取GitHub仓库指定分支的最新提交哈希值。""" + repo_info = GithubUtils.parse_github_url(repo_url) + repo_name = repo_info.repo.replace(".git", "") + return await self._get_newest_commit(repo_info.owner, repo_name, branch) + async def _get_newest_commit(self, owner: str, repo: str, branch: str) -> str: """ 获取仓库最新提交ID