zhenxun_bot/zhenxun/utils/repo_utils/aliyun_manager.py

533 lines
17 KiB
Python
Raw Permalink Normal View History

:sparkles: 支持git更新(github与aliyun codeup),插件商店支持aliyun codeup (#1999) * ✨ feat(env): 支持git更新 * ✨ feat(aliyun): 更新阿里云URL构建逻辑,支持组织名称并优化令牌解码处理 * ✨ feat(config): 修改错误提示信息,更新基础配置文件名称为.env.example * :zap: 插件商店支持aliyun * ✨ feat(store): 优化插件数据获取逻辑,合并插件列表和额外插件列表 * :bug: 修复非git仓库的初始化更新 * ✨ feat(update): 增强更新提示信息,添加非git源的变更文件说明 * :art: 代码格式化 * :sparkles: webui与resources支持git更新 * ✨ feat(update): 更新webui路径处理逻辑 * Fix/test_runwork (#2001) * fix(test): 修复测试工作流 - 修改自动更新模块中的导入路径 - 更新插件商店模块中的插件信息获取逻辑 - 优化插件添加、更新和移除流程 - 统一插件相关错误信息的格式 - 调整测试用例以适应新的插件管理逻辑 * test(builtin_plugins): 重构插件商店相关测试 - 移除 jsd 相关测试用例,只保留 gh(GitHub)的测试 - 删除了 test_plugin_store.py 文件,清理了插件商店的测试 - 更新了 test_search_plugin.py 中的插件版本号 - 调整了 test_update_plugin.py 中的已加载插件版本 - 移除了 StoreManager 类中的 is_external 变量 - 更新了 RepoFileManager 类中的文件获取逻辑,优先使用 GitHub * ✨ feat(submodule): 添加子模块管理功能,支持子模块的初始化、更新和信息获取 * ✨ feat(update): 移除资源管理器,重构更新逻辑,支持通过ZhenxunRepoManager进行资源和Web UI的更新 * test(auto_update): 修改更新检测消息格式 (#2003) - 移除了不必要的版本号后缀(如 "-e6f17c4") - 统一了版本更新消息的格式,删除了冗余信息 * :bug: 修复web zip更新路径问题 * :zap: 文件获取优化使用ali * Fix/test (#2008) * test: 修复bot测试 - 在 test_check_update.py 中跳过两个测试函数 - 移除 test_check.py 中的 mocked_api 参数和相关调用 - 删除 test_add_plugin.py 中的多个测试函数 - 移除 test_remove_plugin.py 中的 mocked_api 参数和相关调用 - 删除 test_search_plugin.py 中的多个测试函数 - 移除 test_update_all_plugin.py 和 test_update_plugin.py 中的 mocked_api 参数和相关调用 * :rotating_light: auto fix by pre-commit hooks --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> * 修复res zip更新路径问题 * :bug: 修复zhenxun更新zip占用问题 * ✨ feat(update): 优化资源更新逻辑,调整更新路径和消息处理 --------- Co-authored-by: molanp <104612722+molanp@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-08-05 17:49:23 +08:00
"""
阿里云CodeUp仓库管理工具
"""
import asyncio
from collections.abc import Callable
from datetime import datetime
from pathlib import Path
from aiocache import cached
from zhenxun.services.log import logger
from zhenxun.utils.github_utils.models import AliyunFileInfo
from zhenxun.utils.repo_utils.utils import prepare_aliyun_url
:sparkles: 支持git更新(github与aliyun codeup),插件商店支持aliyun codeup (#1999) * ✨ feat(env): 支持git更新 * ✨ feat(aliyun): 更新阿里云URL构建逻辑,支持组织名称并优化令牌解码处理 * ✨ feat(config): 修改错误提示信息,更新基础配置文件名称为.env.example * :zap: 插件商店支持aliyun * ✨ feat(store): 优化插件数据获取逻辑,合并插件列表和额外插件列表 * :bug: 修复非git仓库的初始化更新 * ✨ feat(update): 增强更新提示信息,添加非git源的变更文件说明 * :art: 代码格式化 * :sparkles: webui与resources支持git更新 * ✨ feat(update): 更新webui路径处理逻辑 * Fix/test_runwork (#2001) * fix(test): 修复测试工作流 - 修改自动更新模块中的导入路径 - 更新插件商店模块中的插件信息获取逻辑 - 优化插件添加、更新和移除流程 - 统一插件相关错误信息的格式 - 调整测试用例以适应新的插件管理逻辑 * test(builtin_plugins): 重构插件商店相关测试 - 移除 jsd 相关测试用例,只保留 gh(GitHub)的测试 - 删除了 test_plugin_store.py 文件,清理了插件商店的测试 - 更新了 test_search_plugin.py 中的插件版本号 - 调整了 test_update_plugin.py 中的已加载插件版本 - 移除了 StoreManager 类中的 is_external 变量 - 更新了 RepoFileManager 类中的文件获取逻辑,优先使用 GitHub * ✨ feat(submodule): 添加子模块管理功能,支持子模块的初始化、更新和信息获取 * ✨ feat(update): 移除资源管理器,重构更新逻辑,支持通过ZhenxunRepoManager进行资源和Web UI的更新 * test(auto_update): 修改更新检测消息格式 (#2003) - 移除了不必要的版本号后缀(如 "-e6f17c4") - 统一了版本更新消息的格式,删除了冗余信息 * :bug: 修复web zip更新路径问题 * :zap: 文件获取优化使用ali * Fix/test (#2008) * test: 修复bot测试 - 在 test_check_update.py 中跳过两个测试函数 - 移除 test_check.py 中的 mocked_api 参数和相关调用 - 删除 test_add_plugin.py 中的多个测试函数 - 移除 test_remove_plugin.py 中的 mocked_api 参数和相关调用 - 删除 test_search_plugin.py 中的多个测试函数 - 移除 test_update_all_plugin.py 和 test_update_plugin.py 中的 mocked_api 参数和相关调用 * :rotating_light: auto fix by pre-commit hooks --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> * 修复res zip更新路径问题 * :bug: 修复zhenxun更新zip占用问题 * ✨ feat(update): 优化资源更新逻辑,调整更新路径和消息处理 --------- Co-authored-by: molanp <104612722+molanp@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-08-05 17:49:23 +08:00
from .base_manager import BaseRepoManager
from .config import LOG_COMMAND, RepoConfig
from .exceptions import (
AuthenticationError,
FileNotFoundError,
RepoDownloadError,
RepoNotFoundError,
RepoUpdateError,
)
from .models import (
FileDownloadResult,
RepoCommitInfo,
RepoFileInfo,
RepoType,
RepoUpdateResult,
)
class AliyunCodeupManager(BaseRepoManager):
"""阿里云CodeUp仓库管理工具"""
def __init__(self, config: RepoConfig | None = None):
"""
初始化阿里云CodeUp仓库管理工具
Args:
config: 配置如果为None则使用默认配置
"""
super().__init__(config)
self._client = None
async def update_repo(
self,
repo_url: str,
local_path: Path,
branch: str = "main",
include_patterns: list[str] | None = None,
exclude_patterns: list[str] | None = None,
) -> RepoUpdateResult:
"""
更新阿里云CodeUp仓库
Args:
repo_url: 仓库URL或名称
local_path: 本地保存路径
branch: 分支名称
include_patterns: 包含的文件模式列表 ["*.py", "docs/*.md"]
exclude_patterns: 排除的文件模式列表 ["__pycache__/*", "*.pyc"]
Returns:
RepoUpdateResult: 更新结果
"""
try:
# 检查配置
self._check_config()
# 获取仓库名称从URL中提取
repo_url = repo_url.split("/tree/")[0].split("/")[-1].replace(".git", "")
# 获取仓库最新提交ID
newest_commit = await self._get_newest_commit(repo_url, branch)
# 创建结果对象
result = RepoUpdateResult(
repo_type=RepoType.ALIYUN,
repo_name=repo_url.split("/tree/")[0]
.split("/")[-1]
.replace(".git", ""),
owner=self.config.aliyun_codeup.organization_id,
old_version="", # 将在后面更新
new_version=newest_commit,
)
old_version = await self.read_version_file(local_path)
result.old_version = old_version
# 如果版本相同,则无需更新
if old_version == newest_commit:
result.success = True
logger.debug(
f"仓库 {repo_url.split('/')[-1].replace('.git', '')}"
f" 已是最新版本: {newest_commit[:8]}",
LOG_COMMAND,
)
return result
# 确保本地目录存在
local_path.mkdir(parents=True, exist_ok=True)
# 获取仓库名称从URL中提取
repo_name = repo_url.split("/tree/")[0].split("/")[-1].replace(".git", "")
# 获取变更的文件列表
changed_files = await self._get_changed_files(
repo_name, old_version or None, newest_commit
)
# 过滤文件
if include_patterns or exclude_patterns:
from .utils import filter_files
changed_files = filter_files(
changed_files, include_patterns, exclude_patterns
)
result.changed_files = changed_files
# 下载变更的文件
for file_path in changed_files:
try:
local_file_path = local_path / file_path
await self._download_file(
repo_name, file_path, local_file_path, newest_commit
)
except Exception as e:
logger.error(f"下载文件 {file_path} 失败", LOG_COMMAND, e=e)
# 更新版本文件
await self.write_version_file(local_path, newest_commit)
result.success = True
return result
except RepoUpdateError as e:
logger.error(f"更新仓库失败: {e}")
# 从URL中提取仓库名称
repo_name = repo_url.split("/tree/")[0].split("/")[-1].replace(".git", "")
return RepoUpdateResult(
repo_type=RepoType.ALIYUN,
repo_name=repo_name,
owner=self.config.aliyun_codeup.organization_id,
old_version="",
new_version="",
error_message=str(e),
)
except Exception as e:
logger.error(f"更新仓库失败: {e}")
# 从URL中提取仓库名称
repo_name = repo_url.split("/tree/")[0].split("/")[-1].replace(".git", "")
return RepoUpdateResult(
repo_type=RepoType.ALIYUN,
repo_name=repo_name,
owner=self.config.aliyun_codeup.organization_id,
old_version="",
new_version="",
error_message=str(e),
)
async def download_file(
self,
repo_url: str,
file_path: str,
local_path: Path,
branch: str = "main",
) -> FileDownloadResult:
"""
从阿里云CodeUp下载单个文件
Args:
repo_url: 仓库URL或名称
file_path: 文件在仓库中的路径
local_path: 本地保存路径
branch: 分支名称
Returns:
FileDownloadResult: 下载结果
"""
try:
# 检查配置
self._check_config()
# 获取仓库名称从URL中提取
repo_identifier = (
repo_url.split("/tree/")[0].split("/")[-1].replace(".git", "")
)
# 创建结果对象
result = FileDownloadResult(
repo_type=RepoType.ALIYUN,
repo_name=repo_url.split("/tree/")[0]
.split("/")[-1]
.replace(".git", ""),
file_path=file_path,
version=branch,
)
# 确保本地目录存在
Path(local_path).parent.mkdir(parents=True, exist_ok=True)
# 下载文件
file_size = await self._download_file(
repo_identifier, file_path, local_path, branch
)
result.success = True
result.file_size = file_size
return result
except RepoDownloadError as e:
logger.error(f"下载文件失败: {e}")
# 从URL中提取仓库名称
repo_name = repo_url.split("/tree/")[0].split("/")[-1].replace(".git", "")
return FileDownloadResult(
repo_type=RepoType.ALIYUN,
repo_name=repo_name,
file_path=file_path,
version=branch,
error_message=str(e),
)
except Exception as e:
logger.error(f"下载文件失败: {e}")
# 从URL中提取仓库名称
repo_name = repo_url.split("/tree/")[0].split("/")[-1].replace(".git", "")
return FileDownloadResult(
repo_type=RepoType.ALIYUN,
repo_name=repo_name,
file_path=file_path,
version=branch,
error_message=str(e),
)
async def get_file_list(
self,
repo_url: str,
dir_path: str = "",
branch: str = "main",
recursive: bool = False,
) -> list[RepoFileInfo]:
"""
获取仓库文件列表
Args:
repo_url: 仓库URL或名称
dir_path: 目录路径空字符串表示仓库根目录
branch: 分支名称
recursive: 是否递归获取子目录
Returns:
list[RepoFileInfo]: 文件信息列表
"""
try:
# 检查配置
self._check_config()
# 获取仓库名称从URL中提取
repo_identifier = (
repo_url.split("/tree/")[0].split("/")[-1].replace(".git", "")
)
# 获取文件列表
search_type = "RECURSIVE" if recursive else "DIRECT"
tree_list = await AliyunFileInfo.get_repository_tree(
repo_identifier, dir_path, branch, search_type
)
result = []
for tree in tree_list:
# 跳过非当前目录的文件(如果不是递归模式)
if (
not recursive
and tree.path != dir_path
and "/" in tree.path.replace(dir_path, "", 1).strip("/")
):
continue
file_info = RepoFileInfo(
path=tree.path,
is_dir=tree.type == "tree",
)
result.append(file_info)
return result
except Exception as e:
logger.error(f"获取文件列表失败: {e}")
return []
async def get_commit_info(
self, repo_url: str, commit_id: str
) -> RepoCommitInfo | None:
"""
获取提交信息
Args:
repo_url: 仓库URL或名称
commit_id: 提交ID
Returns:
Optional[RepoCommitInfo]: 提交信息如果获取失败则返回None
"""
try:
# 检查配置
self._check_config()
# 获取仓库名称从URL中提取
repo_identifier = (
repo_url.split("/tree/")[0].split("/")[-1].replace(".git", "")
)
# 获取提交信息
# 注意这里假设AliyunFileInfo有get_commit_info方法如果没有需要实现
commit_data = await self._get_commit_info(repo_identifier, commit_id)
if not commit_data:
return None
# 解析提交信息
id_value = commit_data.get("id", commit_id)
message_value = commit_data.get("message", "")
author_value = commit_data.get("author_name", "")
date_value = commit_data.get(
"authored_date", datetime.now().isoformat()
).replace("Z", "+00:00")
return RepoCommitInfo(
commit_id=id_value,
message=message_value,
author=author_value,
commit_time=datetime.fromisoformat(date_value),
changed_files=[], # 阿里云API可能没有直接提供变更文件列表
)
except Exception as e:
logger.error(f"获取提交信息失败: {e}")
return None
def _check_config(self):
"""检查配置"""
if not self.config.aliyun_codeup.access_key_id:
raise AuthenticationError("阿里云CodeUp")
if not self.config.aliyun_codeup.access_key_secret:
raise AuthenticationError("阿里云CodeUp")
if not self.config.aliyun_codeup.organization_id:
raise AuthenticationError("阿里云CodeUp")
async def _get_newest_commit(self, repo_name: str, branch: str) -> str:
"""
获取仓库最新提交ID
Args:
repo_name: 仓库名称
branch: 分支名称
Returns:
str: 提交ID
"""
try:
newest_commit = await AliyunFileInfo.get_newest_commit(repo_name, branch)
if not newest_commit:
raise RepoNotFoundError(repo_name)
return newest_commit
except Exception as e:
logger.error(f"获取最新提交ID失败: {e}")
raise RepoUpdateError(f"获取最新提交ID失败: {e}")
async def _get_commit_info(self, repo_name: str, commit_id: str) -> dict:
"""
获取提交信息
Args:
repo_name: 仓库名称
commit_id: 提交ID
Returns:
dict: 提交信息
"""
# 这里需要实现从阿里云获取提交信息的逻辑
# 由于AliyunFileInfo可能没有get_commit_info方法这里提供一个简单的实现
try:
# 这里应该是调用阿里云API获取提交信息
# 这里只是一个示例实际上需要根据阿里云API实现
return {
"id": commit_id,
"message": "提交信息",
"author_name": "作者",
"authored_date": datetime.now().isoformat(),
}
except Exception as e:
logger.error(f"获取提交信息失败: {e}")
return {}
@cached(ttl=3600)
async def _get_changed_files(
self, repo_name: str, old_commit: str | None, new_commit: str
) -> list[str]:
"""
获取两个提交之间变更的文件列表
Args:
repo_name: 仓库名称
old_commit: 旧提交ID如果为None则获取所有文件
new_commit: 新提交ID
Returns:
list[str]: 变更的文件列表
"""
if not old_commit:
# 如果没有旧提交,则获取仓库中的所有文件
tree_list = await AliyunFileInfo.get_repository_tree(
repo_name, "", new_commit, "RECURSIVE"
)
return [tree.path for tree in tree_list if tree.type == "blob"]
# 获取两个提交之间的差异
try:
return []
except Exception as e:
logger.error(f"获取提交差异失败: {e}")
raise RepoUpdateError(f"获取提交差异失败: {e}")
async def update_via_git(
self,
repo_url: str,
local_path: Path,
branch: str = "main",
force: bool = False,
*,
repo_type: RepoType | None = None,
owner: str | None = None,
prepare_repo_url: Callable[[str], str] | None = None,
) -> RepoUpdateResult:
"""
通过Git命令直接更新仓库
参数:
repo_url: 仓库名称
local_path: 本地仓库路径
branch: 分支名称
force: 是否强制拉取
返回:
RepoUpdateResult: 更新结果
"""
# 调用基类的update_via_git方法
return await super().update_via_git(
repo_url=repo_url,
local_path=local_path,
branch=branch,
force=force,
repo_type=RepoType.ALIYUN,
owner=self.config.aliyun_codeup.organization_id,
prepare_repo_url=prepare_aliyun_url,
)
async def update(
self,
repo_url: str,
local_path: Path,
branch: str = "main",
use_git: bool = True,
force: bool = False,
include_patterns: list[str] | None = None,
exclude_patterns: list[str] | None = None,
) -> RepoUpdateResult:
"""
更新仓库可选择使用Git命令或API方式
参数:
repo_url: 仓库名称
local_path: 本地保存路径
branch: 分支名称
use_git: 是否使用Git命令更新
include_patterns: 包含的文件模式列表 ["*.py", "docs/*.md"]
exclude_patterns: 排除的文件模式列表 ["__pycache__/*", "*.pyc"]
返回:
RepoUpdateResult: 更新结果
"""
if use_git:
return await self.update_via_git(repo_url, local_path, branch, force)
else:
return await self.update_repo(
repo_url, local_path, branch, include_patterns, exclude_patterns
)
async def _download_file(
self, repo_name: str, file_path: str, local_path: Path, ref: str
) -> int:
"""
下载文件
Args:
repo_name: 仓库名称
file_path: 文件在仓库中的路径
local_path: 本地保存路径
ref: 分支/标签/提交ID
Returns:
int: 文件大小字节
"""
# 确保目录存在
local_path.parent.mkdir(parents=True, exist_ok=True)
# 获取文件内容
for retry in range(self.config.aliyun_codeup.download_retry + 1):
try:
content = await AliyunFileInfo.get_file_content(
file_path, repo_name, ref
)
if content is None:
raise FileNotFoundError(file_path, repo_name)
# 保存文件
return await self.save_file_content(content.encode("utf-8"), local_path)
except FileNotFoundError as e:
# 这些错误不需要重试
raise e
except Exception as e:
if retry < self.config.aliyun_codeup.download_retry:
logger.warning("下载文件失败,将重试", LOG_COMMAND, e=e)
await asyncio.sleep(1)
continue
raise RepoDownloadError(f"下载文件失败: {e}")
raise RepoDownloadError("下载文件失败: 超过最大重试次数")