mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-10-04 19:29:59 +08:00
@@ -1,4 +1,3 @@
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
BASE_PATH = Path() / "zhenxun"
|
||||
@@ -10,21 +9,3 @@ DEFAULT_GITHUB_URL = "https://github.com/zhenxun-org/zhenxun_bot_plugins/tree/ma
|
||||
|
||||
EXTRA_GITHUB_URL = "https://github.com/zhenxun-org/zhenxun_bot_plugins_index/tree/index"
|
||||
"""插件库索引github仓库地址"""
|
||||
|
||||
GITHUB_REPO_URL_PATTERN = re.compile(
|
||||
r"^https://github.com/(?P<owner>[^/]+)/(?P<repo>[^/]+)(/tree/(?P<branch>[^/]+))?$"
|
||||
)
|
||||
"""github仓库地址正则"""
|
||||
|
||||
JSD_PACKAGE_API_FORMAT = (
|
||||
"https://data.jsdelivr.com/v1/packages/gh/{owner}/{repo}@{branch}"
|
||||
)
|
||||
"""jsdelivr包地址格式"""
|
||||
|
||||
GIT_API_TREES_FORMAT = (
|
||||
"https://api.github.com/repos/{owner}/{repo}/git/trees/{branch}?recursive=1"
|
||||
)
|
||||
"""git api trees地址格式"""
|
||||
|
||||
CACHED_API_TTL = 300
|
||||
"""缓存api ttl"""
|
||||
|
||||
@@ -8,14 +8,11 @@ from aiocache import cached
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.http_utils import AsyncHttpx
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.utils.github_utils.models import RepoAPI
|
||||
from zhenxun.utils.github_utils import api_strategy, parse_github_url
|
||||
from zhenxun.builtin_plugins.plugin_store.models import StorePluginInfo
|
||||
from zhenxun.utils.image_utils import RowStyle, BuildImage, ImageTemplate
|
||||
from zhenxun.builtin_plugins.auto_update.config import REQ_TXT_FILE_STRING
|
||||
from zhenxun.builtin_plugins.plugin_store.models import (
|
||||
BaseAPI,
|
||||
RepoInfo,
|
||||
PackageApi,
|
||||
StorePluginInfo,
|
||||
)
|
||||
|
||||
from .config import BASE_PATH, EXTRA_GITHUB_URL, DEFAULT_GITHUB_URL
|
||||
|
||||
@@ -81,12 +78,12 @@ class ShopManage:
|
||||
返回:
|
||||
dict: 插件信息数据
|
||||
"""
|
||||
default_github_url = await RepoInfo.parse_github_url(
|
||||
default_github_url = await parse_github_url(
|
||||
DEFAULT_GITHUB_URL
|
||||
).get_download_url_with_path("plugins.json")
|
||||
extra_github_url = await RepoInfo.parse_github_url(
|
||||
).get_raw_download_url("plugins.json")
|
||||
extra_github_url = await parse_github_url(
|
||||
EXTRA_GITHUB_URL
|
||||
).get_download_url_with_path("plugins.json")
|
||||
).get_raw_download_url("plugins.json")
|
||||
res = await AsyncHttpx.get(default_github_url)
|
||||
res2 = await AsyncHttpx.get(extra_github_url)
|
||||
|
||||
@@ -211,29 +208,26 @@ class ShopManage:
|
||||
async def install_plugin_with_repo(
|
||||
cls, github_url: str, module_path: str, is_dir: bool, is_external: bool = False
|
||||
):
|
||||
package_api: PackageApi
|
||||
files: list[str]
|
||||
package_info: BaseAPI
|
||||
repo_info = RepoInfo.parse_github_url(github_url)
|
||||
repo_api: RepoAPI
|
||||
repo_info = parse_github_url(github_url)
|
||||
logger.debug(f"成功获取仓库信息: {repo_info}", "插件管理")
|
||||
for package_api in PackageApi:
|
||||
for repo_api in api_strategy:
|
||||
try:
|
||||
package_info = await package_api.value.parse_repo_info(repo_info)
|
||||
await repo_api.parse_repo_info(repo_info)
|
||||
break
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"获取插件文件失败: {e} | API类型: {package_api.value}", "插件管理"
|
||||
f"获取插件文件失败: {e} | API类型: {repo_api.strategy}", "插件管理"
|
||||
)
|
||||
continue
|
||||
else:
|
||||
raise ValueError("所有API获取插件文件失败,请检查网络连接")
|
||||
files = package_info.get_files(
|
||||
files = repo_api.get_files(
|
||||
module_path=module_path.replace(".", "/") + ("" if is_dir else ".py"),
|
||||
is_dir=is_dir,
|
||||
)
|
||||
download_urls = [
|
||||
await repo_info.get_download_url_with_path(file) for file in files
|
||||
]
|
||||
download_urls = [await repo_info.get_raw_download_url(file) for file in files]
|
||||
base_path = BASE_PATH / "plugins" if is_external else BASE_PATH
|
||||
download_paths: list[Path | str] = [base_path / file for file in files]
|
||||
logger.debug(f"插件下载路径: {download_paths}", "插件管理")
|
||||
@@ -244,11 +238,11 @@ class ShopManage:
|
||||
else:
|
||||
# 安装依赖
|
||||
plugin_path = base_path / "/".join(module_path.split("."))
|
||||
req_files = package_info.get_files(REQ_TXT_FILE_STRING, False)
|
||||
req_files.extend(package_info.get_files("requirement.txt", False))
|
||||
req_files = repo_api.get_files(REQ_TXT_FILE_STRING, False)
|
||||
req_files.extend(repo_api.get_files("requirement.txt", False))
|
||||
logger.debug(f"获取插件依赖文件列表: {req_files}", "插件管理")
|
||||
req_download_urls = [
|
||||
await repo_info.get_download_url_with_path(file) for file in req_files
|
||||
await repo_info.get_raw_download_url(file) for file in req_files
|
||||
]
|
||||
req_paths: list[Path | str] = [plugin_path / file for file in req_files]
|
||||
logger.debug(f"插件依赖文件下载路径: {req_paths}", "插件管理")
|
||||
|
||||
@@ -1,19 +1,6 @@
|
||||
from enum import Enum
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from aiocache import cached
|
||||
from strenum import StrEnum
|
||||
from pydantic import BaseModel
|
||||
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.http_utils import AsyncHttpx
|
||||
|
||||
from .config import (
|
||||
CACHED_API_TTL,
|
||||
GIT_API_TREES_FORMAT,
|
||||
JSD_PACKAGE_API_FORMAT,
|
||||
GITHUB_REPO_URL_PATTERN,
|
||||
)
|
||||
|
||||
type2name: dict[str, str] = {
|
||||
"NORMAL": "普通插件",
|
||||
@@ -41,217 +28,3 @@ class StorePluginInfo(BaseModel):
|
||||
@property
|
||||
def plugin_type_name(self):
|
||||
return type2name[self.plugin_type.value]
|
||||
|
||||
|
||||
class RepoInfo(BaseModel):
|
||||
"""仓库信息"""
|
||||
|
||||
owner: str
|
||||
repo: str
|
||||
branch: str = "main"
|
||||
|
||||
async def get_download_url_with_path(self, path: str):
|
||||
url_format = await self.get_fastest_format()
|
||||
return url_format.format(**self.dict(), path=path)
|
||||
|
||||
@classmethod
|
||||
def parse_github_url(cls, github_url: str) -> "RepoInfo":
|
||||
if matched := GITHUB_REPO_URL_PATTERN.match(github_url):
|
||||
return RepoInfo(**{k: v for k, v in matched.groupdict().items() if v})
|
||||
raise ValueError("github地址格式错误")
|
||||
|
||||
@classmethod
|
||||
@cached()
|
||||
async def get_fastest_format(cls) -> str:
|
||||
return await cls._get_fastest_format()
|
||||
|
||||
@classmethod
|
||||
async def _get_fastest_format(cls) -> str:
|
||||
"""获取最快下载地址格式"""
|
||||
raw_format = "https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{path}"
|
||||
patterns: dict[str, str] = {
|
||||
"https://raw.githubusercontent.com/": raw_format,
|
||||
"https://ghproxy.cc/": f"https://ghproxy.cc/{raw_format}",
|
||||
"https://mirror.ghproxy.com/": f"https://mirror.ghproxy.com/{raw_format}",
|
||||
"https://gh-proxy.com/": f"https://gh-proxy.com/{raw_format}",
|
||||
"https://cdn.jsdelivr.net/": "https://cdn.jsdelivr.net/gh/{owner}/{repo}@{branch}/{path}",
|
||||
}
|
||||
sorted_urls = await AsyncHttpx.get_fastest_mirror(list(patterns.keys()))
|
||||
if not sorted_urls:
|
||||
raise Exception("无法获取任意GitHub资源加速地址,请检查网络")
|
||||
return patterns[sorted_urls[0]]
|
||||
|
||||
|
||||
class FileType(StrEnum):
|
||||
"""文件类型"""
|
||||
|
||||
FILE = "file"
|
||||
DIR = "directory"
|
||||
PACKAGE = "gh"
|
||||
|
||||
|
||||
class BaseAPI(BaseModel, ABC):
|
||||
"""基础接口"""
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
@cached(ttl=CACHED_API_TTL)
|
||||
async def parse_repo_info(cls, repo_info: RepoInfo) -> "BaseAPI": ...
|
||||
|
||||
@abstractmethod
|
||||
def get_files(cls, module_path: str, is_dir) -> list[str]: ...
|
||||
|
||||
|
||||
class JsdelivrAPI(BaseAPI):
|
||||
"""jsdelivr接口"""
|
||||
|
||||
type: FileType
|
||||
name: str
|
||||
files: list["JsdelivrAPI"] = []
|
||||
|
||||
def recurrence_files(self, dir_path: str, is_dir: bool = True) -> list[str]:
|
||||
"""
|
||||
递归获取文件路径
|
||||
|
||||
参数:
|
||||
files: 文件列表
|
||||
dir_path: 目录路径
|
||||
is_dir: 是否为目录
|
||||
|
||||
返回:
|
||||
list[str]: 文件路径
|
||||
"""
|
||||
if not is_dir and dir_path.endswith(self.name):
|
||||
return [dir_path]
|
||||
if self.files is None:
|
||||
raise ValueError("文件列表为空")
|
||||
paths = []
|
||||
for file in self.files:
|
||||
if is_dir and file.type == FileType.DIR and file.files:
|
||||
paths.extend(self.recurrence_files(f"{dir_path}/{file.name}", is_dir))
|
||||
elif file.type == FileType.FILE:
|
||||
if is_dir:
|
||||
paths.append(f"{dir_path}/{file.name}")
|
||||
elif dir_path.endswith(file.name):
|
||||
paths.append(dir_path)
|
||||
return paths
|
||||
|
||||
def full_files_path(self, module_path: str, is_dir: bool = True) -> "JsdelivrAPI":
|
||||
"""
|
||||
获取文件路径
|
||||
|
||||
参数:
|
||||
module_path: 模块路径
|
||||
is_dir: 是否为目录
|
||||
|
||||
返回:
|
||||
list[FileInfo]: 文件路径
|
||||
"""
|
||||
paths: list[str] = module_path.split("/")
|
||||
if not is_dir:
|
||||
paths = paths[:-1]
|
||||
cur_file: JsdelivrAPI = self
|
||||
|
||||
for path in paths:
|
||||
for file in cur_file.files:
|
||||
if file.type == FileType.DIR and file.name == path and file.files:
|
||||
cur_file = file
|
||||
break
|
||||
else:
|
||||
raise ValueError(f"模块路径 {module_path} 不存在")
|
||||
return cur_file
|
||||
|
||||
@classmethod
|
||||
@cached(ttl=CACHED_API_TTL)
|
||||
async def parse_repo_info(cls, repo_info: RepoInfo) -> "JsdelivrAPI":
|
||||
"""解析仓库信息"""
|
||||
|
||||
"""获取插件包信息
|
||||
|
||||
参数:
|
||||
repo_info: 仓库信息
|
||||
|
||||
返回:
|
||||
FileInfo: 插件包信息
|
||||
"""
|
||||
jsd_package_url: str = JSD_PACKAGE_API_FORMAT.format(
|
||||
owner=repo_info.owner, repo=repo_info.repo, branch=repo_info.branch
|
||||
)
|
||||
res = await AsyncHttpx.get(url=jsd_package_url)
|
||||
if res.status_code != 200:
|
||||
raise ValueError(f"下载错误, code: {res.status_code}")
|
||||
return JsdelivrAPI(**res.json())
|
||||
|
||||
def get_files(self, module_path: str, is_dir: bool = True) -> list[str]:
|
||||
"""获取文件路径"""
|
||||
|
||||
file = self.full_files_path(module_path, is_dir)
|
||||
files = file.recurrence_files(
|
||||
module_path,
|
||||
is_dir,
|
||||
)
|
||||
return files
|
||||
|
||||
|
||||
class TreeType(StrEnum):
|
||||
"""树类型"""
|
||||
|
||||
FILE = "blob"
|
||||
DIR = "tree"
|
||||
|
||||
|
||||
class Tree(BaseModel):
|
||||
"""树"""
|
||||
|
||||
path: str
|
||||
mode: str
|
||||
type: TreeType
|
||||
sha: str
|
||||
size: int | None
|
||||
url: str
|
||||
|
||||
|
||||
class GitHubAPI(BaseAPI):
|
||||
"""github接口"""
|
||||
|
||||
sha: str
|
||||
url: str
|
||||
tree: list[Tree]
|
||||
|
||||
def export_files(self, module_path: str) -> list[str]:
|
||||
"""导出文件路径"""
|
||||
return [
|
||||
file.path
|
||||
for file in self.tree
|
||||
if file.type == TreeType.FILE and file.path.startswith(module_path)
|
||||
]
|
||||
|
||||
@classmethod
|
||||
@cached(ttl=CACHED_API_TTL)
|
||||
async def parse_repo_info(cls, repo_info: RepoInfo) -> "GitHubAPI":
|
||||
"""获取仓库树
|
||||
|
||||
参数:
|
||||
repo_info: 仓库信息
|
||||
|
||||
返回:
|
||||
TreesInfo: 仓库树信息
|
||||
"""
|
||||
git_tree_url: str = GIT_API_TREES_FORMAT.format(
|
||||
owner=repo_info.owner, repo=repo_info.repo, branch=repo_info.branch
|
||||
)
|
||||
res = await AsyncHttpx.get(url=git_tree_url)
|
||||
if res.status_code != 200:
|
||||
raise ValueError(f"下载错误, code: {res.status_code}")
|
||||
return GitHubAPI(**res.json())
|
||||
|
||||
def get_files(self, module_path: str, is_dir: bool = True) -> list[str]:
|
||||
"""获取文件路径"""
|
||||
return self.export_files(module_path)
|
||||
|
||||
|
||||
class PackageApi(Enum):
|
||||
"""插件包接口"""
|
||||
|
||||
GITHUB = GitHubAPI
|
||||
JSDELIVR = JsdelivrAPI
|
||||
|
||||
Reference in New Issue
Block a user