From d10cb840683ad526968babc147fbf69bb355ba19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E8=8B=B7=E6=99=B6?= <2749332490@qq.com> Date: Sun, 23 Mar 2025 22:55:07 +0800 Subject: [PATCH 1/9] fix: fix SSLCertVerificationError --- astrbot/core/utils/io.py | 12 +- astrbot/core/zip_updator.py | 17 +- astrbot/dashboard/routes/plugin.py | 435 +++++++++++++---------------- requirements.txt | 3 +- 4 files changed, 227 insertions(+), 240 deletions(-) diff --git a/astrbot/core/utils/io.py b/astrbot/core/utils/io.py index 318a61835..ba09a9dd2 100644 --- a/astrbot/core/utils/io.py +++ b/astrbot/core/utils/io.py @@ -8,6 +8,9 @@ import base64 import zipfile import uuid import psutil + +import certifi + from typing import Union from PIL import Image @@ -81,7 +84,9 @@ async def download_image_by_url( 下载图片, 返回 path """ try: - async with aiohttp.ClientSession(trust_env=True) as session: + ssl_context = ssl.create_default_context(cafile=certifi.where()) # 使用 certifi 提供的 CA 证书 + connector = aiohttp.TCPConnector(ssl=ssl_context) # 使用 certifi 的根证书 + async with aiohttp.ClientSession(trust_env=True, connector=connector) as session: if post: async with session.post(url, json=post_data) as resp: if not path: @@ -118,7 +123,9 @@ async def download_file(url: str, path: str, show_progress: bool = False): 从指定 url 下载文件到指定路径 path """ try: - async with aiohttp.ClientSession(trust_env=True) as session: + ssl_context = ssl.create_default_context(cafile=certifi.where()) # 使用 certifi 提供的 CA 证书 + connector = aiohttp.TCPConnector(ssl=ssl_context) + async with aiohttp.ClientSession(trust_env=True, connector=connector) as session: async with session.get(url, timeout=1800) as resp: if resp.status != 200: raise Exception(f"下载文件失败: {resp.status}") @@ -202,6 +209,7 @@ async def download_dashboard(): """下载管理面板文件""" dashboard_release_url = "https://astrbot-registry.soulter.top/download/astrbot-dashboard/latest/dist.zip" try: + ssl_context = ssl.create_default_context(cafile=certifi.where()) # 使用 certifi 提供的 CA 证书 await download_file( dashboard_release_url, "data/dashboard.zip", show_progress=True ) diff --git a/astrbot/core/zip_updator.py b/astrbot/core/zip_updator.py index 29533ea88..cc951d257 100644 --- a/astrbot/core/zip_updator.py +++ b/astrbot/core/zip_updator.py @@ -2,6 +2,10 @@ import aiohttp import os import zipfile import shutil + +import ssl +import certifi + from astrbot.core.utils.io import on_error, download_file from astrbot.core import logger @@ -33,10 +37,18 @@ class RepoZipUpdator: 返回一个列表,每个元素是一个字典,包含版本号、发布时间、更新内容、commit hash等信息。 """ try: - async with aiohttp.ClientSession(trust_env=True) as session: + ssl_context = ssl.create_default_context(cafile=certifi.where()) # 新增:创建基于 certifi 的 SSL 上下文 + connector = aiohttp.TCPConnector(ssl=ssl_context) # 新增:使用 TCPConnector 指定 SSL 上下文 + async with aiohttp.ClientSession(trust_env=True, connector=connector) as session: async with session.get(url) as response: + # 检查 HTTP 状态码 + if response.status != 200: + text = await response.text() + logger.error(f"请求 {url} 失败,状态码: {response.status}, 内容: {text}") + raise Exception(f"请求失败,状态码: {response.status}") result = await response.json() if not result: + logger.error("返回空的结果喵♡~") return [] # if latest: # ret = self.github_api_release_parser([result[0]]) @@ -53,7 +65,8 @@ class RepoZipUpdator: "zipball_url": release["zipball_url"], } ) - except BaseException: + except Exception as e: + logger.error(f"解析版本信息时发生异常: {e}") raise Exception("解析版本信息失败") return ret diff --git a/astrbot/dashboard/routes/plugin.py b/astrbot/dashboard/routes/plugin.py index 6e90d73e6..ba09a9dd2 100644 --- a/astrbot/dashboard/routes/plugin.py +++ b/astrbot/dashboard/routes/plugin.py @@ -1,260 +1,225 @@ -import traceback +import os +import ssl +import shutil +import socket +import time import aiohttp -from .route import Route, Response, RouteContext -from astrbot.core import logger -from quart import request -from astrbot.core.star.star_manager import PluginManager -from astrbot.core.core_lifecycle import AstrBotCoreLifecycle -from astrbot.core.star.star_handler import star_handlers_registry -from astrbot.core.star.filter.command import CommandFilter -from astrbot.core.star.filter.command_group import CommandGroupFilter -from astrbot.core.star.filter.permission import PermissionTypeFilter -from astrbot.core.star.filter.regex import RegexFilter -from astrbot.core.star.star_handler import EventType +import base64 +import zipfile +import uuid +import psutil + +import certifi + +from typing import Union + +from PIL import Image -class PluginRoute(Route): - def __init__( - self, - context: RouteContext, - core_lifecycle: AstrBotCoreLifecycle, - plugin_manager: PluginManager, - ) -> None: - super().__init__(context) - self.routes = { - "/plugin/get": ("GET", self.get_plugins), - "/plugin/install": ("POST", self.install_plugin), - "/plugin/install-upload": ("POST", self.install_plugin_upload), - "/plugin/update": ("POST", self.update_plugin), - "/plugin/uninstall": ("POST", self.uninstall_plugin), - "/plugin/market_list": ("GET", self.get_online_plugins), - "/plugin/off": ("POST", self.off_plugin), - "/plugin/on": ("POST", self.on_plugin), - "/plugin/reload": ("POST", self.reload_plugins), - } - self.core_lifecycle = core_lifecycle - self.plugin_manager = plugin_manager - self.register_routes() +def on_error(func, path, exc_info): + """ + a callback of the rmtree function. + """ + print(f"remove {path} failed.") + import stat - self.translated_event_type = { - EventType.AdapterMessageEvent: "平台消息下发时", - EventType.OnLLMRequestEvent: "LLM 请求时", - EventType.OnLLMResponseEvent: "LLM 响应后", - EventType.OnDecoratingResultEvent: "回复消息前", - EventType.OnCallingFuncToolEvent: "函数工具", - EventType.OnAfterMessageSentEvent: "发送消息后", - } + if not os.access(path, os.W_OK): + os.chmod(path, stat.S_IWUSR) + func(path) + else: + raise - async def reload_plugins(self): - data = await request.json - plugin_name = data.get("name", None) - try: - success, message = await self.plugin_manager.reload(plugin_name) - if not success: - return Response().error(message).__dict__ - return Response().ok(None, "重载成功。").__dict__ - except Exception as e: - logger.error(f"/api/plugin/reload: {traceback.format_exc()}") - return Response().error(str(e)).__dict__ - async def get_online_plugins(self): - custom = request.args.get("custom_registry") +def remove_dir(file_path) -> bool: + if not os.path.exists(file_path): + return True + try: + shutil.rmtree(file_path, onerror=on_error) + return True + except BaseException: + return False - if custom: - urls = [custom] - else: - urls = ["https://api.soulter.top/astrbot/plugins"] - for url in urls: - try: - async with aiohttp.ClientSession(trust_env=True) as session: - async with session.get(url) as response: - if response.status == 200: - result = await response.json() - return Response().ok(result).__dict__ - else: - logger.error(f"请求 {url} 失败,状态码:{response.status}") - except Exception as e: - logger.error(f"请求 {url} 失败,错误:{e}") +def port_checker(port: int, host: str = "localhost"): + sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sk.settimeout(1) + try: + sk.connect((host, port)) + sk.close() + return True + except Exception: + sk.close() + return False - return Response().error("获取插件列表失败").__dict__ - async def get_plugins(self): - _plugin_resp = [] - for plugin in self.plugin_manager.context.get_all_stars(): - _t = { - "name": plugin.name, - "repo": "" if plugin.repo is None else plugin.repo, - "author": plugin.author, - "desc": plugin.desc, - "version": plugin.version, - "reserved": plugin.reserved, - "activated": plugin.activated, - "online_vesion": "", - "handlers": await self.get_plugin_handlers_info( - plugin.star_handler_full_names - ), - } - _plugin_resp.append(_t) - return ( - Response() - .ok(_plugin_resp, message=self.plugin_manager.failed_plugin_info) - .__dict__ - ) +def save_temp_img(img: Union[Image.Image, str]) -> str: + os.makedirs("data/temp", exist_ok=True) + # 获得文件创建时间,清除超过 12 小时的 + try: + for f in os.listdir("data/temp"): + path = os.path.join("data/temp", f) + if os.path.isfile(path): + ctime = os.path.getctime(path) + if time.time() - ctime > 3600 * 12: + os.remove(path) + except Exception as e: + print(f"清除临时文件失败: {e}") - async def get_plugin_handlers_info(self, handler_full_names: list[str]): - """解析插件行为""" - handlers = [] + # 获得时间戳 + timestamp = f"{int(time.time())}_{uuid.uuid4().hex[:8]}" + p = f"data/temp/{timestamp}.jpg" - for handler_full_name in handler_full_names: - info = {} - handler = star_handlers_registry.star_handlers_map.get( - handler_full_name, None - ) - if handler is None: - continue - info["event_type"] = handler.event_type.name - info["event_type_h"] = self.translated_event_type.get( - handler.event_type, handler.event_type.name - ) - info["handler_full_name"] = handler.handler_full_name - info["desc"] = handler.desc - info["handler_name"] = handler.handler_name + if isinstance(img, Image.Image): + img.save(p) + else: + with open(p, "wb") as f: + f.write(img) + return p - if handler.event_type == EventType.AdapterMessageEvent: - # 处理平台适配器消息事件 - has_admin = False - for filter in ( - handler.event_filters - ): # 正常handler就只有 1~2 个 filter,因此这里时间复杂度不会太高 - if isinstance(filter, CommandFilter): - info["type"] = "指令" - info["cmd"] = ( - f"{filter.parent_command_names[0]} {filter.command_name}" - ) - info["cmd"] = info["cmd"].strip() - if ( - self.core_lifecycle.astrbot_config["wake_prefix"] - and len(self.core_lifecycle.astrbot_config["wake_prefix"]) - > 0 - ): - info["cmd"] = ( - f"{self.core_lifecycle.astrbot_config['wake_prefix'][0]}{info['cmd']}" - ) - elif isinstance(filter, CommandGroupFilter): - info["type"] = "指令组" - info["cmd"] = filter.get_complete_command_names()[0] - info["cmd"] = info["cmd"].strip() - info["sub_command"] = filter.print_cmd_tree( - filter.sub_command_filters - ) - if ( - self.core_lifecycle.astrbot_config["wake_prefix"] - and len(self.core_lifecycle.astrbot_config["wake_prefix"]) - > 0 - ): - info["cmd"] = ( - f"{self.core_lifecycle.astrbot_config['wake_prefix'][0]}{info['cmd']}" - ) - elif isinstance(filter, RegexFilter): - info["type"] = "正则匹配" - info["cmd"] = filter.regex_str - elif isinstance(filter, PermissionTypeFilter): - has_admin = True - info["has_admin"] = has_admin - if "cmd" not in info: - info["cmd"] = "未知" - if "type" not in info: - info["type"] = "事件监听器" + +async def download_image_by_url( + url: str, post: bool = False, post_data: dict = None, path=None +) -> str: + """ + 下载图片, 返回 path + """ + try: + ssl_context = ssl.create_default_context(cafile=certifi.where()) # 使用 certifi 提供的 CA 证书 + connector = aiohttp.TCPConnector(ssl=ssl_context) # 使用 certifi 的根证书 + async with aiohttp.ClientSession(trust_env=True, connector=connector) as session: + if post: + async with session.post(url, json=post_data) as resp: + if not path: + return save_temp_img(await resp.read()) + else: + with open(path, "wb") as f: + f.write(await resp.read()) + return path else: - info["cmd"] = "自动触发" - info["type"] = "无" + async with session.get(url) as resp: + if not path: + return save_temp_img(await resp.read()) + else: + with open(path, "wb") as f: + f.write(await resp.read()) + return path + except aiohttp.client.ClientConnectorSSLError: + # 关闭SSL验证 + ssl_context = ssl.create_default_context() + ssl_context.set_ciphers("DEFAULT") + async with aiohttp.ClientSession() as session: + if post: + async with session.get(url, ssl=ssl_context) as resp: + return save_temp_img(await resp.read()) + else: + async with session.get(url, ssl=ssl_context) as resp: + return save_temp_img(await resp.read()) + except Exception as e: + raise e - if not info["desc"]: - info["desc"] = "无描述" - handlers.append(info) +async def download_file(url: str, path: str, show_progress: bool = False): + """ + 从指定 url 下载文件到指定路径 path + """ + try: + ssl_context = ssl.create_default_context(cafile=certifi.where()) # 使用 certifi 提供的 CA 证书 + connector = aiohttp.TCPConnector(ssl=ssl_context) + async with aiohttp.ClientSession(trust_env=True, connector=connector) as session: + async with session.get(url, timeout=1800) as resp: + if resp.status != 200: + raise Exception(f"下载文件失败: {resp.status}") + total_size = int(resp.headers.get("content-length", 0)) + downloaded_size = 0 + start_time = time.time() + if show_progress: + print(f"文件大小: {total_size / 1024:.2f} KB | 文件地址: {url}") + with open(path, "wb") as f: + while True: + chunk = await resp.content.read(8192) + if not chunk: + break + f.write(chunk) + downloaded_size += len(chunk) + if show_progress: + elapsed_time = time.time() - start_time + speed = downloaded_size / 1024 / elapsed_time # KB/s + print( + f"\r下载进度: {downloaded_size / total_size:.2%} 速度: {speed:.2f} KB/s", + end="", + ) + except aiohttp.client.ClientConnectorSSLError: + # 关闭SSL验证 + ssl_context = ssl.create_default_context() + ssl_context.set_ciphers("DEFAULT") + async with aiohttp.ClientSession() as session: + async with session.get(url, ssl=ssl_context, timeout=120) as resp: + total_size = int(resp.headers.get("content-length", 0)) + downloaded_size = 0 + start_time = time.time() + if show_progress: + print(f"文件大小: {total_size / 1024:.2f} KB | 文件地址: {url}") + with open(path, "wb") as f: + while True: + chunk = await resp.content.read(8192) + if not chunk: + break + f.write(chunk) + downloaded_size += len(chunk) + if show_progress: + elapsed_time = time.time() - start_time + speed = downloaded_size / 1024 / elapsed_time # KB/s + print( + f"\r下载进度: {downloaded_size / total_size:.2%} 速度: {speed:.2f} KB/s", + end="", + ) + if show_progress: + print() - return handlers - async def install_plugin(self): - post_data = await request.json - repo_url = post_data["url"] +def file_to_base64(file_path: str) -> str: + with open(file_path, "rb") as f: + data_bytes = f.read() + base64_str = base64.b64encode(data_bytes).decode() + return "base64://" + base64_str - proxy: str = post_data.get("proxy", None) - if proxy: - proxy = proxy.removesuffix("/") - try: - logger.info(f"正在安装插件 {repo_url}") - await self.plugin_manager.install_plugin(repo_url, proxy) - # self.core_lifecycle.restart() - logger.info(f"安装插件 {repo_url} 成功。") - return Response().ok(None, "安装成功。").__dict__ - except Exception as e: - logger.error(traceback.format_exc()) - return Response().error(str(e)).__dict__ +def get_local_ip_addresses(): + net_interfaces = psutil.net_if_addrs() + network_ips = [] - async def install_plugin_upload(self): - try: - file = await request.files - file = file["file"] - logger.info(f"正在安装用户上传的插件 {file.filename}") - file_path = f"data/temp/{file.filename}" - await file.save(file_path) - await self.plugin_manager.install_plugin_from_file(file_path) - # self.core_lifecycle.restart() - logger.info(f"安装插件 {file.filename} 成功") - return Response().ok(None, "安装成功。").__dict__ - except Exception as e: - logger.error(traceback.format_exc()) - return Response().error(str(e)).__dict__ + for interface, addrs in net_interfaces.items(): + for addr in addrs: + if addr.family == socket.AF_INET: # 使用 socket.AF_INET 代替 psutil.AF_INET + network_ips.append(addr.address) - async def uninstall_plugin(self): - post_data = await request.json - plugin_name = post_data["name"] - try: - logger.info(f"正在卸载插件 {plugin_name}") - await self.plugin_manager.uninstall_plugin(plugin_name) - logger.info(f"卸载插件 {plugin_name} 成功") - return Response().ok(None, "卸载成功").__dict__ - except Exception as e: - logger.error(traceback.format_exc()) - return Response().error(str(e)).__dict__ + return network_ips - async def update_plugin(self): - post_data = await request.json - plugin_name = post_data["name"] - proxy: str = post_data.get("proxy", None) - try: - logger.info(f"正在更新插件 {plugin_name}") - await self.plugin_manager.update_plugin(plugin_name, proxy) - # self.core_lifecycle.restart() - await self.plugin_manager.reload(plugin_name) - logger.info(f"更新插件 {plugin_name} 成功。") - return Response().ok(None, "更新成功。").__dict__ - except Exception as e: - logger.error(f"/api/plugin/update: {traceback.format_exc()}") - return Response().error(str(e)).__dict__ - async def off_plugin(self): - post_data = await request.json - plugin_name = post_data["name"] - try: - await self.plugin_manager.turn_off_plugin(plugin_name) - logger.info(f"停用插件 {plugin_name} 。") - return Response().ok(None, "停用成功。").__dict__ - except Exception as e: - logger.error(f"/api/plugin/off: {traceback.format_exc()}") - return Response().error(str(e)).__dict__ +async def get_dashboard_version(): + if os.path.exists("data/dist"): + if os.path.exists("data/dist/assets/version"): + with open("data/dist/assets/version", "r") as f: + v = f.read().strip() + return v + return None - async def on_plugin(self): - post_data = await request.json - plugin_name = post_data["name"] - try: - await self.plugin_manager.turn_on_plugin(plugin_name) - logger.info(f"启用插件 {plugin_name} 。") - return Response().ok(None, "启用成功。").__dict__ - except Exception as e: - logger.error(f"/api/plugin/on: {traceback.format_exc()}") - return Response().error(str(e)).__dict__ + +async def download_dashboard(): + """下载管理面板文件""" + dashboard_release_url = "https://astrbot-registry.soulter.top/download/astrbot-dashboard/latest/dist.zip" + try: + ssl_context = ssl.create_default_context(cafile=certifi.where()) # 使用 certifi 提供的 CA 证书 + await download_file( + dashboard_release_url, "data/dashboard.zip", show_progress=True + ) + except BaseException as _: + dashboard_release_url = ( + "https://github.com/Soulter/AstrBot/releases/latest/download/dist.zip" + ) + await download_file( + dashboard_release_url, "data/dashboard.zip", show_progress=True + ) + print("解压管理面板文件中...") + with zipfile.ZipFile("data/dashboard.zip", "r") as z: + z.extractall("data") diff --git a/requirements.txt b/requirements.txt index 313dba0c8..95983a2e8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,4 +25,5 @@ dashscope python-telegram-bot wechatpy dingtalk-stream -mcp \ No newline at end of file +mcp +certifi \ No newline at end of file From 1cb2b62f81db90e62b9f1574b9231d08bc41ee7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E8=8B=B7=E6=99=B6?= <2749332490@qq.com> Date: Sun, 23 Mar 2025 23:02:34 +0800 Subject: [PATCH 2/9] fix: fix error --- astrbot/dashboard/routes/plugin.py | 444 ++++++++++++++++------------- 1 file changed, 243 insertions(+), 201 deletions(-) diff --git a/astrbot/dashboard/routes/plugin.py b/astrbot/dashboard/routes/plugin.py index ba09a9dd2..af4d0db31 100644 --- a/astrbot/dashboard/routes/plugin.py +++ b/astrbot/dashboard/routes/plugin.py @@ -1,225 +1,267 @@ -import os -import ssl -import shutil -import socket -import time +import traceback import aiohttp -import base64 -import zipfile -import uuid -import psutil +import ssl import certifi -from typing import Union - -from PIL import Image +from .route import Route, Response, RouteContext +from astrbot.core import logger +from quart import request +from astrbot.core.star.star_manager import PluginManager +from astrbot.core.core_lifecycle import AstrBotCoreLifecycle +from astrbot.core.star.star_handler import star_handlers_registry +from astrbot.core.star.filter.command import CommandFilter +from astrbot.core.star.filter.command_group import CommandGroupFilter +from astrbot.core.star.filter.permission import PermissionTypeFilter +from astrbot.core.star.filter.regex import RegexFilter +from astrbot.core.star.star_handler import EventType -def on_error(func, path, exc_info): - """ - a callback of the rmtree function. - """ - print(f"remove {path} failed.") - import stat +class PluginRoute(Route): + def __init__( + self, + context: RouteContext, + core_lifecycle: AstrBotCoreLifecycle, + plugin_manager: PluginManager, + ) -> None: + super().__init__(context) + self.routes = { + "/plugin/get": ("GET", self.get_plugins), + "/plugin/install": ("POST", self.install_plugin), + "/plugin/install-upload": ("POST", self.install_plugin_upload), + "/plugin/update": ("POST", self.update_plugin), + "/plugin/uninstall": ("POST", self.uninstall_plugin), + "/plugin/market_list": ("GET", self.get_online_plugins), + "/plugin/off": ("POST", self.off_plugin), + "/plugin/on": ("POST", self.on_plugin), + "/plugin/reload": ("POST", self.reload_plugins), + } + self.core_lifecycle = core_lifecycle + self.plugin_manager = plugin_manager + self.register_routes() - if not os.access(path, os.W_OK): - os.chmod(path, stat.S_IWUSR) - func(path) - else: - raise + self.translated_event_type = { + EventType.AdapterMessageEvent: "平台消息下发时", + EventType.OnLLMRequestEvent: "LLM 请求时", + EventType.OnLLMResponseEvent: "LLM 响应后", + EventType.OnDecoratingResultEvent: "回复消息前", + EventType.OnCallingFuncToolEvent: "函数工具", + EventType.OnAfterMessageSentEvent: "发送消息后", + } + async def reload_plugins(self): + data = await request.json + plugin_name = data.get("name", None) + try: + success, message = await self.plugin_manager.reload(plugin_name) + if not success: + return Response().error(message).__dict__ + return Response().ok(None, "重载成功。").__dict__ + except Exception as e: + logger.error(f"/api/plugin/reload: {traceback.format_exc()}") + return Response().error(str(e)).__dict__ -def remove_dir(file_path) -> bool: - if not os.path.exists(file_path): - return True - try: - shutil.rmtree(file_path, onerror=on_error) - return True - except BaseException: - return False + async def get_online_plugins(self): + custom = request.args.get("custom_registry") + if custom: + urls = [custom] + else: + urls = ["https://api.soulter.top/astrbot/plugins"] -def port_checker(port: int, host: str = "localhost"): - sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sk.settimeout(1) - try: - sk.connect((host, port)) - sk.close() - return True - except Exception: - sk.close() - return False - - -def save_temp_img(img: Union[Image.Image, str]) -> str: - os.makedirs("data/temp", exist_ok=True) - # 获得文件创建时间,清除超过 12 小时的 - try: - for f in os.listdir("data/temp"): - path = os.path.join("data/temp", f) - if os.path.isfile(path): - ctime = os.path.getctime(path) - if time.time() - ctime > 3600 * 12: - os.remove(path) - except Exception as e: - print(f"清除临时文件失败: {e}") - - # 获得时间戳 - timestamp = f"{int(time.time())}_{uuid.uuid4().hex[:8]}" - p = f"data/temp/{timestamp}.jpg" - - if isinstance(img, Image.Image): - img.save(p) - else: - with open(p, "wb") as f: - f.write(img) - return p - - -async def download_image_by_url( - url: str, post: bool = False, post_data: dict = None, path=None -) -> str: - """ - 下载图片, 返回 path - """ - try: - ssl_context = ssl.create_default_context(cafile=certifi.where()) # 使用 certifi 提供的 CA 证书 - connector = aiohttp.TCPConnector(ssl=ssl_context) # 使用 certifi 的根证书 - async with aiohttp.ClientSession(trust_env=True, connector=connector) as session: - if post: - async with session.post(url, json=post_data) as resp: - if not path: - return save_temp_img(await resp.read()) - else: - with open(path, "wb") as f: - f.write(await resp.read()) - return path - else: - async with session.get(url) as resp: - if not path: - return save_temp_img(await resp.read()) - else: - with open(path, "wb") as f: - f.write(await resp.read()) - return path - except aiohttp.client.ClientConnectorSSLError: - # 关闭SSL验证 - ssl_context = ssl.create_default_context() - ssl_context.set_ciphers("DEFAULT") - async with aiohttp.ClientSession() as session: - if post: - async with session.get(url, ssl=ssl_context) as resp: - return save_temp_img(await resp.read()) - else: - async with session.get(url, ssl=ssl_context) as resp: - return save_temp_img(await resp.read()) - except Exception as e: - raise e - - -async def download_file(url: str, path: str, show_progress: bool = False): - """ - 从指定 url 下载文件到指定路径 path - """ - try: - ssl_context = ssl.create_default_context(cafile=certifi.where()) # 使用 certifi 提供的 CA 证书 + # 新增:创建 SSL 上下文,使用 certifi 提供的根证书 + ssl_context = ssl.create_default_context(cafile=certifi.where()) connector = aiohttp.TCPConnector(ssl=ssl_context) - async with aiohttp.ClientSession(trust_env=True, connector=connector) as session: - async with session.get(url, timeout=1800) as resp: - if resp.status != 200: - raise Exception(f"下载文件失败: {resp.status}") - total_size = int(resp.headers.get("content-length", 0)) - downloaded_size = 0 - start_time = time.time() - if show_progress: - print(f"文件大小: {total_size / 1024:.2f} KB | 文件地址: {url}") - with open(path, "wb") as f: - while True: - chunk = await resp.content.read(8192) - if not chunk: - break - f.write(chunk) - downloaded_size += len(chunk) - if show_progress: - elapsed_time = time.time() - start_time - speed = downloaded_size / 1024 / elapsed_time # KB/s - print( - f"\r下载进度: {downloaded_size / total_size:.2%} 速度: {speed:.2f} KB/s", - end="", + for url in urls: + try: + async with aiohttp.ClientSession(trust_env=True, connector=connector) as session: + async with session.get(url) as response: + if response.status == 200: + result = await response.json() + return Response().ok(result).__dict__ + else: + logger.error(f"请求 {url} 失败,状态码:{response.status}") + except Exception as e: + logger.error(f"请求 {url} 失败,错误:{e}") + + return Response().error("获取插件列表失败").__dict__ + + async def get_plugins(self): + _plugin_resp = [] + for plugin in self.plugin_manager.context.get_all_stars(): + _t = { + "name": plugin.name, + "repo": "" if plugin.repo is None else plugin.repo, + "author": plugin.author, + "desc": plugin.desc, + "version": plugin.version, + "reserved": plugin.reserved, + "activated": plugin.activated, + "online_vesion": "", + "handlers": await self.get_plugin_handlers_info( + plugin.star_handler_full_names + ), + } + _plugin_resp.append(_t) + return ( + Response() + .ok(_plugin_resp, message=self.plugin_manager.failed_plugin_info) + .__dict__ + ) + + async def get_plugin_handlers_info(self, handler_full_names: list[str]): + """解析插件行为""" + handlers = [] + + for handler_full_name in handler_full_names: + info = {} + handler = star_handlers_registry.star_handlers_map.get( + handler_full_name, None + ) + if handler is None: + continue + info["event_type"] = handler.event_type.name + info["event_type_h"] = self.translated_event_type.get( + handler.event_type, handler.event_type.name + ) + info["handler_full_name"] = handler.handler_full_name + info["desc"] = handler.desc + info["handler_name"] = handler.handler_name + + if handler.event_type == EventType.AdapterMessageEvent: + # 处理平台适配器消息事件 + has_admin = False + for filter in ( + handler.event_filters + ): # 正常handler就只有 1~2 个 filter,因此这里时间复杂度不会太高 + if isinstance(filter, CommandFilter): + info["type"] = "指令" + info["cmd"] = ( + f"{filter.parent_command_names[0]} {filter.command_name}" + ) + info["cmd"] = info["cmd"].strip() + if ( + self.core_lifecycle.astrbot_config["wake_prefix"] + and len(self.core_lifecycle.astrbot_config["wake_prefix"]) + > 0 + ): + info["cmd"] = ( + f"{self.core_lifecycle.astrbot_config['wake_prefix'][0]}{info['cmd']}" ) - except aiohttp.client.ClientConnectorSSLError: - # 关闭SSL验证 - ssl_context = ssl.create_default_context() - ssl_context.set_ciphers("DEFAULT") - async with aiohttp.ClientSession() as session: - async with session.get(url, ssl=ssl_context, timeout=120) as resp: - total_size = int(resp.headers.get("content-length", 0)) - downloaded_size = 0 - start_time = time.time() - if show_progress: - print(f"文件大小: {total_size / 1024:.2f} KB | 文件地址: {url}") - with open(path, "wb") as f: - while True: - chunk = await resp.content.read(8192) - if not chunk: - break - f.write(chunk) - downloaded_size += len(chunk) - if show_progress: - elapsed_time = time.time() - start_time - speed = downloaded_size / 1024 / elapsed_time # KB/s - print( - f"\r下载进度: {downloaded_size / total_size:.2%} 速度: {speed:.2f} KB/s", - end="", + elif isinstance(filter, CommandGroupFilter): + info["type"] = "指令组" + info["cmd"] = filter.get_complete_command_names()[0] + info["cmd"] = info["cmd"].strip() + info["sub_command"] = filter.print_cmd_tree( + filter.sub_command_filters + ) + if ( + self.core_lifecycle.astrbot_config["wake_prefix"] + and len(self.core_lifecycle.astrbot_config["wake_prefix"]) + > 0 + ): + info["cmd"] = ( + f"{self.core_lifecycle.astrbot_config['wake_prefix'][0]}{info['cmd']}" ) - if show_progress: - print() + elif isinstance(filter, RegexFilter): + info["type"] = "正则匹配" + info["cmd"] = filter.regex_str + elif isinstance(filter, PermissionTypeFilter): + has_admin = True + info["has_admin"] = has_admin + if "cmd" not in info: + info["cmd"] = "未知" + if "type" not in info: + info["type"] = "事件监听器" + else: + info["cmd"] = "自动触发" + info["type"] = "无" + if not info["desc"]: + info["desc"] = "无描述" -def file_to_base64(file_path: str) -> str: - with open(file_path, "rb") as f: - data_bytes = f.read() - base64_str = base64.b64encode(data_bytes).decode() - return "base64://" + base64_str + handlers.append(info) + return handlers -def get_local_ip_addresses(): - net_interfaces = psutil.net_if_addrs() - network_ips = [] + async def install_plugin(self): + post_data = await request.json + repo_url = post_data["url"] - for interface, addrs in net_interfaces.items(): - for addr in addrs: - if addr.family == socket.AF_INET: # 使用 socket.AF_INET 代替 psutil.AF_INET - network_ips.append(addr.address) + proxy: str = post_data.get("proxy", None) + if proxy: + proxy = proxy.removesuffix("/") - return network_ips + try: + logger.info(f"正在安装插件 {repo_url}") + await self.plugin_manager.install_plugin(repo_url, proxy) + # self.core_lifecycle.restart() + logger.info(f"安装插件 {repo_url} 成功。") + return Response().ok(None, "安装成功。").__dict__ + except Exception as e: + logger.error(traceback.format_exc()) + return Response().error(str(e)).__dict__ + async def install_plugin_upload(self): + try: + file = await request.files + file = file["file"] + logger.info(f"正在安装用户上传的插件 {file.filename}") + file_path = f"data/temp/{file.filename}" + await file.save(file_path) + await self.plugin_manager.install_plugin_from_file(file_path) + # self.core_lifecycle.restart() + logger.info(f"安装插件 {file.filename} 成功") + return Response().ok(None, "安装成功。").__dict__ + except Exception as e: + logger.error(traceback.format_exc()) + return Response().error(str(e)).__dict__ -async def get_dashboard_version(): - if os.path.exists("data/dist"): - if os.path.exists("data/dist/assets/version"): - with open("data/dist/assets/version", "r") as f: - v = f.read().strip() - return v - return None + async def uninstall_plugin(self): + post_data = await request.json + plugin_name = post_data["name"] + try: + logger.info(f"正在卸载插件 {plugin_name}") + await self.plugin_manager.uninstall_plugin(plugin_name) + logger.info(f"卸载插件 {plugin_name} 成功") + return Response().ok(None, "卸载成功").__dict__ + except Exception as e: + logger.error(traceback.format_exc()) + return Response().error(str(e)).__dict__ + async def update_plugin(self): + post_data = await request.json + plugin_name = post_data["name"] + proxy: str = post_data.get("proxy", None) + try: + logger.info(f"正在更新插件 {plugin_name}") + await self.plugin_manager.update_plugin(plugin_name, proxy) + # self.core_lifecycle.restart() + await self.plugin_manager.reload(plugin_name) + logger.info(f"更新插件 {plugin_name} 成功。") + return Response().ok(None, "更新成功。").__dict__ + except Exception as e: + logger.error(f"/api/plugin/update: {traceback.format_exc()}") + return Response().error(str(e)).__dict__ -async def download_dashboard(): - """下载管理面板文件""" - dashboard_release_url = "https://astrbot-registry.soulter.top/download/astrbot-dashboard/latest/dist.zip" - try: - ssl_context = ssl.create_default_context(cafile=certifi.where()) # 使用 certifi 提供的 CA 证书 - await download_file( - dashboard_release_url, "data/dashboard.zip", show_progress=True - ) - except BaseException as _: - dashboard_release_url = ( - "https://github.com/Soulter/AstrBot/releases/latest/download/dist.zip" - ) - await download_file( - dashboard_release_url, "data/dashboard.zip", show_progress=True - ) - print("解压管理面板文件中...") - with zipfile.ZipFile("data/dashboard.zip", "r") as z: - z.extractall("data") + async def off_plugin(self): + post_data = await request.json + plugin_name = post_data["name"] + try: + await self.plugin_manager.turn_off_plugin(plugin_name) + logger.info(f"停用插件 {plugin_name} 。") + return Response().ok(None, "停用成功。").__dict__ + except Exception as e: + logger.error(f"/api/plugin/off: {traceback.format_exc()}") + return Response().error(str(e)).__dict__ + + async def on_plugin(self): + post_data = await request.json + plugin_name = post_data["name"] + try: + await self.plugin_manager.turn_on_plugin(plugin_name) + logger.info(f"启用插件 {plugin_name} 。") + return Response().ok(None, "启用成功。").__dict__ + except Exception as e: + logger.error(f"/api/plugin/on: {traceback.format_exc()}") + return Response().error(str(e)).__dict__ From b669b3145152804de172dcc85682d37496dc19b4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 23 Mar 2025 15:07:22 +0000 Subject: [PATCH 3/9] :balloon: auto fixes by pre-commit hooks --- astrbot/core/utils/io.py | 20 +++++++++++++++----- astrbot/core/zip_updator.py | 16 ++++++++++++---- astrbot/dashboard/routes/plugin.py | 4 +++- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/astrbot/core/utils/io.py b/astrbot/core/utils/io.py index ba09a9dd2..1cdb2a409 100644 --- a/astrbot/core/utils/io.py +++ b/astrbot/core/utils/io.py @@ -84,9 +84,13 @@ async def download_image_by_url( 下载图片, 返回 path """ try: - ssl_context = ssl.create_default_context(cafile=certifi.where()) # 使用 certifi 提供的 CA 证书 + ssl_context = ssl.create_default_context( + cafile=certifi.where() + ) # 使用 certifi 提供的 CA 证书 connector = aiohttp.TCPConnector(ssl=ssl_context) # 使用 certifi 的根证书 - async with aiohttp.ClientSession(trust_env=True, connector=connector) as session: + async with aiohttp.ClientSession( + trust_env=True, connector=connector + ) as session: if post: async with session.post(url, json=post_data) as resp: if not path: @@ -123,9 +127,13 @@ async def download_file(url: str, path: str, show_progress: bool = False): 从指定 url 下载文件到指定路径 path """ try: - ssl_context = ssl.create_default_context(cafile=certifi.where()) # 使用 certifi 提供的 CA 证书 + ssl_context = ssl.create_default_context( + cafile=certifi.where() + ) # 使用 certifi 提供的 CA 证书 connector = aiohttp.TCPConnector(ssl=ssl_context) - async with aiohttp.ClientSession(trust_env=True, connector=connector) as session: + async with aiohttp.ClientSession( + trust_env=True, connector=connector + ) as session: async with session.get(url, timeout=1800) as resp: if resp.status != 200: raise Exception(f"下载文件失败: {resp.status}") @@ -209,7 +217,9 @@ async def download_dashboard(): """下载管理面板文件""" dashboard_release_url = "https://astrbot-registry.soulter.top/download/astrbot-dashboard/latest/dist.zip" try: - ssl_context = ssl.create_default_context(cafile=certifi.where()) # 使用 certifi 提供的 CA 证书 + ssl_context = ssl.create_default_context( + cafile=certifi.where() + ) # 使用 certifi 提供的 CA 证书 await download_file( dashboard_release_url, "data/dashboard.zip", show_progress=True ) diff --git a/astrbot/core/zip_updator.py b/astrbot/core/zip_updator.py index cc951d257..4f17b0277 100644 --- a/astrbot/core/zip_updator.py +++ b/astrbot/core/zip_updator.py @@ -37,14 +37,22 @@ class RepoZipUpdator: 返回一个列表,每个元素是一个字典,包含版本号、发布时间、更新内容、commit hash等信息。 """ try: - ssl_context = ssl.create_default_context(cafile=certifi.where()) # 新增:创建基于 certifi 的 SSL 上下文 - connector = aiohttp.TCPConnector(ssl=ssl_context) # 新增:使用 TCPConnector 指定 SSL 上下文 - async with aiohttp.ClientSession(trust_env=True, connector=connector) as session: + ssl_context = ssl.create_default_context( + cafile=certifi.where() + ) # 新增:创建基于 certifi 的 SSL 上下文 + connector = aiohttp.TCPConnector( + ssl=ssl_context + ) # 新增:使用 TCPConnector 指定 SSL 上下文 + async with aiohttp.ClientSession( + trust_env=True, connector=connector + ) as session: async with session.get(url) as response: # 检查 HTTP 状态码 if response.status != 200: text = await response.text() - logger.error(f"请求 {url} 失败,状态码: {response.status}, 内容: {text}") + logger.error( + f"请求 {url} 失败,状态码: {response.status}, 内容: {text}" + ) raise Exception(f"请求失败,状态码: {response.status}") result = await response.json() if not result: diff --git a/astrbot/dashboard/routes/plugin.py b/astrbot/dashboard/routes/plugin.py index af4d0db31..32dc99176 100644 --- a/astrbot/dashboard/routes/plugin.py +++ b/astrbot/dashboard/routes/plugin.py @@ -74,7 +74,9 @@ class PluginRoute(Route): connector = aiohttp.TCPConnector(ssl=ssl_context) for url in urls: try: - async with aiohttp.ClientSession(trust_env=True, connector=connector) as session: + async with aiohttp.ClientSession( + trust_env=True, connector=connector + ) as session: async with session.get(url) as response: if response.status == 200: result = await response.json() From 4db14b905fed4cb4ac7516e01dc4f91b460543cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E8=8B=B7=E6=99=B6?= <2749332490@qq.com> Date: Sun, 23 Mar 2025 23:40:06 +0800 Subject: [PATCH 4/9] fix: fix error --- astrbot/core/utils/io.py | 3 --- astrbot/core/zip_updator.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/astrbot/core/utils/io.py b/astrbot/core/utils/io.py index 1cdb2a409..7393cb424 100644 --- a/astrbot/core/utils/io.py +++ b/astrbot/core/utils/io.py @@ -217,9 +217,6 @@ async def download_dashboard(): """下载管理面板文件""" dashboard_release_url = "https://astrbot-registry.soulter.top/download/astrbot-dashboard/latest/dist.zip" try: - ssl_context = ssl.create_default_context( - cafile=certifi.where() - ) # 使用 certifi 提供的 CA 证书 await download_file( dashboard_release_url, "data/dashboard.zip", show_progress=True ) diff --git a/astrbot/core/zip_updator.py b/astrbot/core/zip_updator.py index 4f17b0277..4622b47cd 100644 --- a/astrbot/core/zip_updator.py +++ b/astrbot/core/zip_updator.py @@ -56,7 +56,7 @@ class RepoZipUpdator: raise Exception(f"请求失败,状态码: {response.status}") result = await response.json() if not result: - logger.error("返回空的结果喵♡~") + logger.error("返回空的结果") return [] # if latest: # ret = self.github_api_release_parser([result[0]]) From ddf54c9cf84f1f3a1985367c902ebdbd8cb5a75e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 24 Mar 2025 04:32:20 +0000 Subject: [PATCH 5/9] :balloon: auto fixes by pre-commit hooks --- astrbot/dashboard/routes/stat.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/astrbot/dashboard/routes/stat.py b/astrbot/dashboard/routes/stat.py index 04b2d21ee..a74187977 100644 --- a/astrbot/dashboard/routes/stat.py +++ b/astrbot/dashboard/routes/stat.py @@ -69,10 +69,10 @@ class StatRoute(Route): process = psutil.Process() # 获取系统CPU使用率而不是进程CPU使用率 cpu_percent = psutil.cpu_percent(interval=0.5) - + # 获取线程数 thread_count = threading.active_count() - + # 获取插件信息 plugins = self.core_lifecycle.star_context.get_all_stars() plugin_info = [] @@ -80,7 +80,7 @@ class StatRoute(Route): info = { "name": getattr(plugin, "name", plugin.__class__.__name__), "version": getattr(plugin, "version", "1.0.0"), - "is_enabled": True + "is_enabled": True, } plugin_info.append(info) From 6b0f04419821af7fa5d9ab512548acdb7bf02b80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E8=8B=B7=E6=99=B6?= <2749332490@qq.com> Date: Mon, 24 Mar 2025 13:20:05 +0800 Subject: [PATCH 6/9] fix: fix other errors --- astrbot/core/utils/t2i/local_strategy.py | 5 ++++- astrbot/core/utils/t2i/network_strategy.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/astrbot/core/utils/t2i/local_strategy.py b/astrbot/core/utils/t2i/local_strategy.py index 23abba6e6..ccd0da0fc 100644 --- a/astrbot/core/utils/t2i/local_strategy.py +++ b/astrbot/core/utils/t2i/local_strategy.py @@ -1,5 +1,6 @@ import re import aiohttp +import ssl, certifi from io import BytesIO from . import RenderStrategy @@ -91,7 +92,9 @@ class LocalRenderStrategy(RenderStrategy): try: image_url = re.findall(IMAGE_REGEX, line)[0] print(image_url) - async with aiohttp.ClientSession(trust_env=True) as session: + ssl_context = ssl.create_default_context(cafile=certifi.where()) + connector = aiohttp.TCPConnector(ssl=ssl_context) + async with aiohttp.ClientSession(trust_env=True, connector=connector) as session: async with session.get(image_url) as resp: image_res = Image.open(BytesIO(await resp.read())) images[i] = image_res diff --git a/astrbot/core/utils/t2i/network_strategy.py b/astrbot/core/utils/t2i/network_strategy.py index b9b9abffe..72a5ecf48 100644 --- a/astrbot/core/utils/t2i/network_strategy.py +++ b/astrbot/core/utils/t2i/network_strategy.py @@ -1,5 +1,6 @@ import aiohttp import os +import ssl, certifi from . import RenderStrategy from astrbot.core.config import VERSION @@ -46,7 +47,9 @@ class NetworkRenderStrategy(RenderStrategy): }, } if return_url: - async with aiohttp.ClientSession(trust_env=True) as session: + ssl_context = ssl.create_default_context(cafile=certifi.where()) + connector = aiohttp.TCPConnector(ssl=ssl_context) + async with aiohttp.ClientSession(trust_env=True, connector=connector) as session: async with session.post( f"{self.BASE_RENDER_URL}/generate", json=post_data ) as resp: From 7334090ac11818c9cd2a1763ea6edff865773657 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 24 Mar 2025 05:20:36 +0000 Subject: [PATCH 7/9] :balloon: auto fixes by pre-commit hooks --- astrbot/core/utils/t2i/network_strategy.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/astrbot/core/utils/t2i/network_strategy.py b/astrbot/core/utils/t2i/network_strategy.py index 72a5ecf48..b253a5023 100644 --- a/astrbot/core/utils/t2i/network_strategy.py +++ b/astrbot/core/utils/t2i/network_strategy.py @@ -49,7 +49,9 @@ class NetworkRenderStrategy(RenderStrategy): if return_url: ssl_context = ssl.create_default_context(cafile=certifi.where()) connector = aiohttp.TCPConnector(ssl=ssl_context) - async with aiohttp.ClientSession(trust_env=True, connector=connector) as session: + async with aiohttp.ClientSession( + trust_env=True, connector=connector + ) as session: async with session.post( f"{self.BASE_RENDER_URL}/generate", json=post_data ) as resp: From 1df33ac3c8161fdd4051440a4b085c610062e455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E8=8B=B7=E6=99=B6?= <2749332490@qq.com> Date: Mon, 24 Mar 2025 13:28:14 +0800 Subject: [PATCH 8/9] fix: fix error --- astrbot/core/utils/t2i/local_strategy.py | 3 ++- astrbot/core/utils/t2i/network_strategy.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/astrbot/core/utils/t2i/local_strategy.py b/astrbot/core/utils/t2i/local_strategy.py index ccd0da0fc..aea3f4815 100644 --- a/astrbot/core/utils/t2i/local_strategy.py +++ b/astrbot/core/utils/t2i/local_strategy.py @@ -1,6 +1,7 @@ import re import aiohttp -import ssl, certifi +import ssl +import certifi from io import BytesIO from . import RenderStrategy diff --git a/astrbot/core/utils/t2i/network_strategy.py b/astrbot/core/utils/t2i/network_strategy.py index b253a5023..f8f54b0dc 100644 --- a/astrbot/core/utils/t2i/network_strategy.py +++ b/astrbot/core/utils/t2i/network_strategy.py @@ -1,6 +1,7 @@ import aiohttp import os -import ssl, certifi +import ssl +import certifi from . import RenderStrategy from astrbot.core.config import VERSION From 0a43e4672e47c4754f563a2f4f7a97278c7306a7 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 24 Mar 2025 17:57:28 +0800 Subject: [PATCH 9/9] style: format codes --- astrbot/dashboard/routes/stat.py | 9 ++------- dashboard/src/views/PlatformPage.vue | 6 +++--- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/astrbot/dashboard/routes/stat.py b/astrbot/dashboard/routes/stat.py index 04b2d21ee..e73c09455 100644 --- a/astrbot/dashboard/routes/stat.py +++ b/astrbot/dashboard/routes/stat.py @@ -65,14 +65,9 @@ class StatRoute(Route): stat_dict = stat.__dict__ - # 获取CPU使用率 - 修复CPU始终为0的问题 - process = psutil.Process() - # 获取系统CPU使用率而不是进程CPU使用率 cpu_percent = psutil.cpu_percent(interval=0.5) - - # 获取线程数 thread_count = threading.active_count() - + # 获取插件信息 plugins = self.core_lifecycle.star_context.get_all_stars() plugin_info = [] @@ -80,7 +75,7 @@ class StatRoute(Route): info = { "name": getattr(plugin, "name", plugin.__class__.__name__), "version": getattr(plugin, "version", "1.0.0"), - "is_enabled": True + "is_enabled": True, } plugin_info.append(info) diff --git a/dashboard/src/views/PlatformPage.vue b/dashboard/src/views/PlatformPage.vue index 38fa08e68..1af951306 100644 --- a/dashboard/src/views/PlatformPage.vue +++ b/dashboard/src/views/PlatformPage.vue @@ -96,7 +96,7 @@ - + {{ updatingMode ? 'mdi-pencil' : 'mdi-plus' }} @@ -105,12 +105,12 @@ - + - + mdi-refresh 刷新