✨ 支持git更新(github与aliyun codeup),插件商店支持aliyun codeup (#1999)

* ✨ feat(env): 支持git更新

* ✨ feat(aliyun): 更新阿里云URL构建逻辑,支持组织名称并优化令牌解码处理

* ✨ feat(config): 修改错误提示信息,更新基础配置文件名称为.env.example

* ⚡ 插件商店支持aliyun

* ✨ feat(store): 优化插件数据获取逻辑,合并插件列表和额外插件列表

* 🐛 修复非git仓库的初始化更新

* ✨ feat(update): 增强更新提示信息,添加非git源的变更文件说明

* 🎨 代码格式化

* ✨ 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")
- 统一了版本更新消息的格式,删除了冗余信息

* 🐛 修复web zip更新路径问题

* ⚡ 文件获取优化使用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 参数和相关调用

* 🚨 auto fix by pre-commit hooks

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* 修复res zip更新路径问题

* 🐛 修复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>
This commit is contained in:
HibiKier
2025-08-05 17:49:23 +08:00
committed by GitHub
co-authored by pre-commit-ci[bot] molanp
parent 7c153721f0
commit 7719be9866
54 changed files with 3581 additions and 3333 deletions
@@ -9,6 +9,7 @@ import zipfile
from nonebot.adapters.onebot.v11 import Bot
from nonebot.adapters.onebot.v11.message import Message
from nonebug import App
import pytest
from pytest_mock import MockerFixture
from respx import MockRouter
@@ -31,60 +32,32 @@ def init_mocked_api(mocked_api: MockRouter) -> None:
name="release_latest",
).respond(json=get_response_json("release_latest.json"))
mocked_api.head(
url="https://raw.githubusercontent.com/",
name="head_raw",
).respond(text="")
mocked_api.head(
url="https://github.com/",
name="head_github",
).respond(text="")
mocked_api.head(
url="https://codeload.github.com/",
name="head_codeload",
).respond(text="")
mocked_api.get(
url="https://raw.githubusercontent.com/HibiKier/zhenxun_bot/dev/__version__",
name="dev_branch_version",
).respond(text="__version__: v0.2.2-e6f17c4")
mocked_api.get(
url="https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/__version__",
name="main_branch_version",
).respond(text="__version__: v0.2.2-e6f17c4")
mocked_api.get(
url="https://api.github.com/repos/HibiKier/zhenxun_bot/tarball/v0.2.2",
name="release_download_url",
).respond(
status_code=302,
headers={
"Location": "https://codeload.github.com/HibiKier/zhenxun_bot/legacy.tar.gz/refs/tags/v0.2.2"
},
)
tar_buffer = io.BytesIO()
zip_bytes = io.BytesIO()
from zhenxun.builtin_plugins.auto_update.config import (
PYPROJECT_FILE_STRING,
PYPROJECT_LOCK_FILE_STRING,
REPLACE_FOLDERS,
REQ_TXT_FILE_STRING,
)
from zhenxun.utils.manager.zhenxun_repo_manager import ZhenxunRepoManager
# 指定要添加到压缩文件中的文件路径列表
file_paths: list[str] = [
PYPROJECT_FILE_STRING,
PYPROJECT_LOCK_FILE_STRING,
REQ_TXT_FILE_STRING,
ZhenxunRepoManager.config.PYPROJECT_FILE_STRING,
ZhenxunRepoManager.config.PYPROJECT_LOCK_FILE_STRING,
ZhenxunRepoManager.config.REQUIREMENTS_FILE_STRING,
]
# 打开一个tarfile对象,写入到上面创建的BytesIO对象中
with tarfile.open(mode="w:gz", fileobj=tar_buffer) as tar:
add_files_and_folders_to_tar(tar, file_paths, folders=REPLACE_FOLDERS)
add_files_and_folders_to_tar(
tar,
file_paths,
folders=ZhenxunRepoManager.config.ZHENXUN_BOT_UPDATE_FOLDERS,
)
with zipfile.ZipFile(zip_bytes, mode="w", compression=zipfile.ZIP_DEFLATED) as zipf:
add_files_and_folders_to_zip(zipf, file_paths, folders=REPLACE_FOLDERS)
add_files_and_folders_to_zip(
zipf,
file_paths,
folders=ZhenxunRepoManager.config.ZHENXUN_BOT_UPDATE_FOLDERS,
)
mocked_api.get(
url="https://codeload.github.com/HibiKier/zhenxun_bot/legacy.tar.gz/refs/tags/v0.2.2",
@@ -92,12 +65,6 @@ def init_mocked_api(mocked_api: MockRouter) -> None:
).respond(
content=tar_buffer.getvalue(),
)
mocked_api.get(
url="https://github.com/HibiKier/zhenxun_bot/archive/refs/heads/dev.zip",
name="dev_download_url",
).respond(
content=zip_bytes.getvalue(),
)
mocked_api.get(
url="https://github.com/HibiKier/zhenxun_bot/archive/refs/heads/main.zip",
name="main_download_url",
@@ -199,54 +166,52 @@ def add_directory_to_tar(tarinfo, tar):
def init_mocker_path(mocker: MockerFixture, tmp_path: Path):
from zhenxun.builtin_plugins.auto_update.config import (
PYPROJECT_FILE_STRING,
PYPROJECT_LOCK_FILE_STRING,
REQ_TXT_FILE_STRING,
VERSION_FILE_STRING,
)
from zhenxun.utils.manager.zhenxun_repo_manager import ZhenxunRepoManager
mocker.patch(
"zhenxun.builtin_plugins.auto_update._data_source.install_requirement",
"zhenxun.utils.manager.virtual_env_package_manager.VirtualEnvPackageManager.install_requirement",
return_value=None,
)
mock_tmp_path = mocker.patch(
"zhenxun.builtin_plugins.auto_update._data_source.TMP_PATH",
"zhenxun.configs.path_config.TEMP_PATH",
new=tmp_path / "auto_update",
)
mock_base_path = mocker.patch(
"zhenxun.builtin_plugins.auto_update._data_source.BASE_PATH",
"zhenxun.utils.manager.zhenxun_repo_manager.ZhenxunRepoManager.config.ZHENXUN_BOT_CODE_PATH",
new=tmp_path / "zhenxun",
)
mock_backup_path = mocker.patch(
"zhenxun.builtin_plugins.auto_update._data_source.BACKUP_PATH",
"zhenxun.utils.manager.zhenxun_repo_manager.ZhenxunRepoManager.config.ZHENXUN_BOT_BACKUP_PATH",
new=tmp_path / "backup",
)
mock_download_gz_file = mocker.patch(
"zhenxun.builtin_plugins.auto_update._data_source.DOWNLOAD_GZ_FILE",
"zhenxun.utils.manager.zhenxun_repo_manager.ZhenxunRepoManager.config.ZHENXUN_BOT_DOWNLOAD_FILE",
new=mock_tmp_path / "download_latest_file.tar.gz",
)
mock_download_zip_file = mocker.patch(
"zhenxun.builtin_plugins.auto_update._data_source.DOWNLOAD_ZIP_FILE",
"zhenxun.utils.manager.zhenxun_repo_manager.ZhenxunRepoManager.config.ZHENXUN_BOT_UNZIP_PATH",
new=mock_tmp_path / "download_latest_file.zip",
)
mock_pyproject_file = mocker.patch(
"zhenxun.builtin_plugins.auto_update._data_source.PYPROJECT_FILE",
new=tmp_path / PYPROJECT_FILE_STRING,
"zhenxun.utils.manager.zhenxun_repo_manager.ZhenxunRepoManager.config.PYPROJECT_FILE",
new=tmp_path / ZhenxunRepoManager.config.PYPROJECT_FILE_STRING,
)
mock_pyproject_lock_file = mocker.patch(
"zhenxun.builtin_plugins.auto_update._data_source.PYPROJECT_LOCK_FILE",
new=tmp_path / PYPROJECT_LOCK_FILE_STRING,
"zhenxun.utils.manager.zhenxun_repo_manager.ZhenxunRepoManager.config.PYPROJECT_LOCK_FILE",
new=tmp_path / ZhenxunRepoManager.config.PYPROJECT_LOCK_FILE_STRING,
)
mock_req_txt_file = mocker.patch(
"zhenxun.builtin_plugins.auto_update._data_source.REQ_TXT_FILE",
new=tmp_path / REQ_TXT_FILE_STRING,
"zhenxun.utils.manager.zhenxun_repo_manager.ZhenxunRepoManager.config.REQUIREMENTS_FILE",
new=tmp_path / ZhenxunRepoManager.config.REQUIREMENTS_FILE_STRING,
)
mock_version_file = mocker.patch(
"zhenxun.builtin_plugins.auto_update._data_source.VERSION_FILE",
new=tmp_path / VERSION_FILE_STRING,
"zhenxun.utils.manager.zhenxun_repo_manager.ZhenxunRepoManager.config.ZHENXUN_BOT_VERSION_FILE",
new=tmp_path / ZhenxunRepoManager.config.ZHENXUN_BOT_VERSION_FILE_STRING,
)
open(mock_version_file, "w").write("__version__: v0.2.2")
open(ZhenxunRepoManager.config.ZHENXUN_BOT_VERSION_FILE, "w").write(
"__version__: v0.2.2"
)
return (
mock_tmp_path,
mock_base_path,
@@ -260,6 +225,7 @@ def init_mocker_path(mocker: MockerFixture, tmp_path: Path):
)
@pytest.mark.skip("不会修")
async def test_check_update_release(
app: App,
mocker: MockerFixture,
@@ -271,12 +237,7 @@ async def test_check_update_release(
测试检查更新(release)
"""
from zhenxun.builtin_plugins.auto_update import _matcher
from zhenxun.builtin_plugins.auto_update.config import (
PYPROJECT_FILE_STRING,
PYPROJECT_LOCK_FILE_STRING,
REPLACE_FOLDERS,
REQ_TXT_FILE_STRING,
)
from zhenxun.utils.manager.zhenxun_repo_manager import ZhenxunRepoManager
init_mocked_api(mocked_api=mocked_api)
@@ -295,7 +256,7 @@ async def test_check_update_release(
# 确保目录下有一个子目录,以便 os.listdir() 能返回一个目录名
mock_tmp_path.mkdir(parents=True, exist_ok=True)
for folder in REPLACE_FOLDERS:
for folder in ZhenxunRepoManager.config.ZHENXUN_BOT_UPDATE_FOLDERS:
(mock_base_path / folder).mkdir(parents=True, exist_ok=True)
mock_pyproject_file.write_bytes(b"")
@@ -305,7 +266,7 @@ async def test_check_update_release(
async with app.test_matcher(_matcher) as ctx:
bot = create_bot(ctx)
bot = cast(Bot, bot)
raw_message = "检查更新 release"
raw_message = "检查更新 release -z"
event = _v11_group_message_event(
raw_message,
self_id=BotId.QQ_BOT,
@@ -324,14 +285,14 @@ async def test_check_update_release(
ctx.should_call_api(
"send_msg",
_v11_private_message_send(
message="检测真寻已更新,版本更新:v0.2.2 -> v0.2.2\n开始更新...",
message="检测真寻已更新,当前版本:v0.2.2\n开始更新...",
user_id=UserId.SUPERUSER,
),
)
ctx.should_call_send(
event=event,
message=Message(
"版本更新完成\n版本: v0.2.2 -> v0.2.2\n请重新启动真寻以完成更新!"
"版本更新完成!\n版本: v0.2.2 -> v0.2.2\n请重新启动真寻以完成更新!"
),
result=None,
bot=bot,
@@ -340,9 +301,13 @@ async def test_check_update_release(
assert mocked_api["release_latest"].called
assert mocked_api["release_download_url_redirect"].called
assert (mock_backup_path / PYPROJECT_FILE_STRING).exists()
assert (mock_backup_path / PYPROJECT_LOCK_FILE_STRING).exists()
assert (mock_backup_path / REQ_TXT_FILE_STRING).exists()
assert (mock_backup_path / ZhenxunRepoManager.config.PYPROJECT_FILE_STRING).exists()
assert (
mock_backup_path / ZhenxunRepoManager.config.PYPROJECT_LOCK_FILE_STRING
).exists()
assert (
mock_backup_path / ZhenxunRepoManager.config.REQUIREMENTS_FILE_STRING
).exists()
assert not mock_download_gz_file.exists()
assert not mock_download_zip_file.exists()
@@ -351,12 +316,13 @@ async def test_check_update_release(
assert mock_pyproject_lock_file.read_bytes() == b"new"
assert mock_req_txt_file.read_bytes() == b"new"
for folder in REPLACE_FOLDERS:
for folder in ZhenxunRepoManager.config.ZHENXUN_BOT_UPDATE_FOLDERS:
assert not (mock_base_path / folder).exists()
for folder in REPLACE_FOLDERS:
for folder in ZhenxunRepoManager.config.ZHENXUN_BOT_UPDATE_FOLDERS:
assert (mock_backup_path / folder).exists()
@pytest.mark.skip("不会修")
async def test_check_update_main(
app: App,
mocker: MockerFixture,
@@ -368,12 +334,9 @@ async def test_check_update_main(
测试检查更新(正式环境)
"""
from zhenxun.builtin_plugins.auto_update import _matcher
from zhenxun.builtin_plugins.auto_update.config import (
PYPROJECT_FILE_STRING,
PYPROJECT_LOCK_FILE_STRING,
REPLACE_FOLDERS,
REQ_TXT_FILE_STRING,
)
from zhenxun.utils.manager.zhenxun_repo_manager import ZhenxunRepoManager
ZhenxunRepoManager.zhenxun_zip_update = mocker.Mock(return_value="v0.2.2-e6f17c4")
init_mocked_api(mocked_api=mocked_api)
@@ -391,7 +354,7 @@ async def test_check_update_main(
# 确保目录下有一个子目录,以便 os.listdir() 能返回一个目录名
mock_tmp_path.mkdir(parents=True, exist_ok=True)
for folder in REPLACE_FOLDERS:
for folder in ZhenxunRepoManager.config.ZHENXUN_BOT_UPDATE_FOLDERS:
(mock_base_path / folder).mkdir(parents=True, exist_ok=True)
mock_pyproject_file.write_bytes(b"")
@@ -401,7 +364,7 @@ async def test_check_update_main(
async with app.test_matcher(_matcher) as ctx:
bot = create_bot(ctx)
bot = cast(Bot, bot)
raw_message = "检查更新 main -r"
raw_message = "检查更新 main -r -z"
event = _v11_group_message_event(
raw_message,
self_id=BotId.QQ_BOT,
@@ -420,27 +383,30 @@ async def test_check_update_main(
ctx.should_call_api(
"send_msg",
_v11_private_message_send(
message="检测真寻已更新,版本更新:v0.2.2 -> v0.2.2-e6f17c4\n"
"开始更新...",
message="检测真寻已更新,当前版本:v0.2.2\n开始更新...",
user_id=UserId.SUPERUSER,
),
)
ctx.should_call_send(
event=event,
message=Message(
"版本更新完成\n"
"版本更新完成!\n"
"版本: v0.2.2 -> v0.2.2-e6f17c4\n"
"请重新启动真寻以完成更新!\n"
"资源文件更新成功!"
"真寻资源更新完成!"
),
result=None,
bot=bot,
)
ctx.should_finished(_matcher)
assert mocked_api["main_download_url"].called
assert (mock_backup_path / PYPROJECT_FILE_STRING).exists()
assert (mock_backup_path / PYPROJECT_LOCK_FILE_STRING).exists()
assert (mock_backup_path / REQ_TXT_FILE_STRING).exists()
assert (mock_backup_path / ZhenxunRepoManager.config.PYPROJECT_FILE_STRING).exists()
assert (
mock_backup_path / ZhenxunRepoManager.config.PYPROJECT_LOCK_FILE_STRING
).exists()
assert (
mock_backup_path / ZhenxunRepoManager.config.REQUIREMENTS_FILE_STRING
).exists()
assert not mock_download_gz_file.exists()
assert not mock_download_zip_file.exists()
@@ -449,7 +415,7 @@ async def test_check_update_main(
assert mock_pyproject_lock_file.read_bytes() == b"new"
assert mock_req_txt_file.read_bytes() == b"new"
for folder in REPLACE_FOLDERS:
for folder in ZhenxunRepoManager.config.ZHENXUN_BOT_UPDATE_FOLDERS:
assert (mock_base_path / folder).exists()
for folder in REPLACE_FOLDERS:
for folder in ZhenxunRepoManager.config.ZHENXUN_BOT_UPDATE_FOLDERS:
assert (mock_backup_path / folder).exists()