From eb1fade6f532214b3dd27c757bb3b87c1f66fef6 Mon Sep 17 00:00:00 2001 From: DLmaster361 Date: Sat, 19 Jul 2025 21:38:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20AUTO=5FMAA=20=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=88=86=E4=BA=AB=E4=B8=AD=E5=BF=83=E4=B8=8A=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/config.py | 2 +- app/core/network.py | 121 +++++++++++++++++-- app/ui/Widget.py | 9 +- app/ui/home.py | 2 +- app/ui/member_manager.py | 248 ++++++++++++++++++++++++++++++++++++++- app/ui/setting.py | 4 +- main.py | 6 +- resources/version.json | 6 + 8 files changed, 380 insertions(+), 18 deletions(-) diff --git a/app/core/config.py b/app/core/config.py index 76e8bfe..a9bcbd1 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -597,7 +597,7 @@ class GeneralConfig(LQConfig): super().__init__() self.Script_Name = ConfigItem("Script", "Name", "") - self.Script_RootPath = ConfigItem("Script", "RootPath", ".", FolderValidator()) + self.Script_RootPath = ConfigItem("Script", "RootPath", ".", FileValidator()) self.Script_ScriptPath = ConfigItem( "Script", "ScriptPath", ".", FileValidator() ) diff --git a/app/core/network.py b/app/core/network.py index 15211ee..79b54a5 100644 --- a/app/core/network.py +++ b/app/core/network.py @@ -31,6 +31,7 @@ import time import requests import truststore from pathlib import Path +from typing import Dict from .logger import logger @@ -42,7 +43,14 @@ class NetworkThread(QThread): timeout = 10 backoff_factor = 0.1 - def __init__(self, mode: str, url: str, path: Path = None) -> None: + def __init__( + self, + mode: str, + url: str, + path: Path = None, + files: Dict = None, + data: Dict = None, + ) -> None: super().__init__() self.setObjectName( @@ -54,6 +62,8 @@ class NetworkThread(QThread): self.mode = mode self.url = url self.path = path + self.files = files + self.data = data from .config import Config @@ -78,6 +88,8 @@ class NetworkThread(QThread): self.get_json(self.url) elif self.mode == "get_file": self.get_file(self.url, self.path) + elif self.mode == "upload_file": + self.upload_file(self.url, self.files, self.data) def get_json(self, url: str) -> None: """ @@ -102,7 +114,7 @@ class NetworkThread(QThread): self.response_json = None self.error_message = str(e) logger.exception( - f"子线程 {self.objectName()} 网络请求失败:{e}", + f"子线程 {self.objectName()} 网络请求失败:{e},第{_+1}次尝试", module="网络请求子线程", ) time.sleep(self.backoff_factor) @@ -122,14 +134,15 @@ class NetworkThread(QThread): response = None try: - response = requests.get(url, timeout=10, proxies=self.proxies) + response = requests.get(url, timeout=self.timeout, proxies=self.proxies) if response.status_code == 200: with open(path, "wb") as file: file.write(response.content) self.status_code = response.status_code + self.error_message = None else: self.status_code = response.status_code - self.error_message = "下载失败" + self.error_message = f"下载失败,状态码: {response.status_code}" except Exception as e: self.status_code = response.status_code if response else None @@ -140,6 +153,54 @@ class NetworkThread(QThread): self.loop.quit() + def upload_file(self, url: str, files: Dict, data: Dict = None) -> None: + """ + 通过POST方法上传文件 + + :param url: 请求的URL + :param files: 文件字典,格式为 {'file': ('filename', file_obj, 'content_type')} + :param data: 表单数据字典 + """ + + logger.info(f"子线程 {self.objectName()} 开始上传文件", module="网络请求子线程") + + response = None + + for _ in range(self.max_retries): + try: + response = requests.post( + url, + files=files, + data=data, + timeout=self.timeout, + proxies=self.proxies, + ) + self.status_code = response.status_code + + print(response.text) + + # 尝试解析JSON响应 + try: + self.response_json = response.json() + except ValueError: + # 如果不是JSON格式,保存文本内容 + self.response_json = {"text": response.text} + + self.error_message = None + break + + except Exception as e: + self.status_code = response.status_code if response else None + self.response_json = None + self.error_message = str(e) + logger.exception( + f"子线程 {self.objectName()} 文件上传失败:{e},第{_+1}次尝试", + module="网络请求子线程", + ) + time.sleep(self.backoff_factor) + + self.loop.quit() + class _Network(QObject): """网络请求线程管理类""" @@ -149,19 +210,28 @@ class _Network(QObject): self.task_queue = [] - def add_task(self, mode: str, url: str, path: Path = None) -> NetworkThread: + def add_task( + self, + mode: str, + url: str, + path: Path = None, + files: Dict = None, + data: Dict = None, + ) -> NetworkThread: """ 添加网络请求任务 - :param mode: 请求模式,支持 "get", "get_file" + :param mode: 请求模式,支持 "get", "get_file", "upload_file" :param url: 请求的URL :param path: 下载文件的保存路径,仅在 mode 为 "get_file" 时有效 + :param files: 上传文件字典,仅在 mode 为 "upload_file" 时有效 + :param data: 表单数据字典,仅在 mode 为 "upload_file" 时有效 :return: 返回创建的 NetworkThread 实例 """ logger.info(f"添加网络请求任务: {mode} {url} {path}", module="网络请求") - network_thread = NetworkThread(mode, url, path) + network_thread = NetworkThread(mode, url, path, files, data) self.task_queue.append(network_thread) @@ -169,6 +239,43 @@ class _Network(QObject): return network_thread + def upload_config_file( + self, file_path: Path, username: str = "", description: str = "" + ) -> NetworkThread: + """ + 上传配置文件到分享服务器 + + :param file_path: 要上传的文件路径 + :param username: 用户名(可选) + :param description: 文件描述(必填) + :return: 返回创建的 NetworkThread 实例 + """ + + if not file_path.exists(): + raise FileNotFoundError(f"文件不存在: {file_path}") + + if not description: + raise ValueError("文件描述不能为空") + + # 准备上传的文件 + with open(file_path, "rb") as f: + files = {"file": (file_path.name, f.read(), "application/json")} + + # 准备表单数据 + data = {"description": description} + + if username: + data["username"] = username + + url = "http://221.236.27.82:10023/api/upload/share" + + logger.info( + f"准备上传配置文件: {file_path.name},用户: {username or '匿名'},描述: {description}", + extra={"module": "网络请求"}, + ) + + return self.add_task("upload_file", url, files=files, data=data) + def get_result(self, network_thread: NetworkThread) -> dict: """ 获取网络请求结果 diff --git a/app/ui/Widget.py b/app/ui/Widget.py index a5c3342..7af06f5 100644 --- a/app/ui/Widget.py +++ b/app/ui/Widget.py @@ -255,7 +255,9 @@ class NoticeMessageBox(MessageBoxBase): self.button_cancel.clicked.connect(self.cancelButton.click) self.index.index_cards[0].clicked.emit() - def __update_text(self, text: str): + def __update_text(self, index: int, text: str): + + self.currentIndex = index html = markdown.markdown(text).replace("\n", "") html = re.sub( @@ -273,7 +275,7 @@ class NoticeMessageBox(MessageBoxBase): class NoticeIndexCard(HeaderCardWidget): - index_changed = Signal(str) + index_changed = Signal(int, str) def __init__(self, title: str, content: Dict[str, str], parent=None): super().__init__(parent) @@ -289,12 +291,13 @@ class NoticeMessageBox(MessageBoxBase): self.index_cards.append(QuantifiedItemCard([index, ""])) self.index_cards[-1].clicked.connect( - partial(self.index_changed.emit, text) + partial(self.index_changed.emit, len(self.index_cards), text) ) self.Layout.addWidget(self.index_cards[-1]) if not content: self.Layout.addWidget(QuantifiedItemCard(["暂无信息", ""])) + self.currentIndex = 0 self.Layout.addStretch(1) diff --git a/app/ui/home.py b/app/ui/home.py index 0e332f7..7cde6f6 100644 --- a/app/ui/home.py +++ b/app/ui/home.py @@ -204,7 +204,7 @@ class Home(QWidget): # 从远程服务器获取最新主题图像信息 network = Network.add_task( mode="get", - url="https://gitee.com/DLmaster_361/AUTO_MAA/raw/server/theme_image.json", + url="http://221.236.27.82:10197/d/AUTO_MAA/Server/theme_image.json", ) network.loop.exec() network_result = Network.get_result(network) diff --git a/app/ui/member_manager.py b/app/ui/member_manager.py index 30704ea..1d6446f 100644 --- a/app/ui/member_manager.py +++ b/app/ui/member_manager.py @@ -54,7 +54,7 @@ from PySide6.QtCore import Signal from datetime import datetime from functools import partial from pathlib import Path -from typing import List, Union, Type +from typing import List, Dict, Union, Type import shutil import json @@ -94,6 +94,7 @@ from .Widget import ( PushAndComboBoxSettingCard, StatusSwitchSetting, UserNoticeSettingCard, + NoticeMessageBox, PivotArea, ) @@ -393,7 +394,7 @@ class MemberManager(QWidget): # 从远程服务器获取应用列表 network = Network.add_task( mode="get", - url="https://gitee.com/DLmaster_361/AUTO_MAA/raw/server/apps_info.json", + url="http://221.236.27.82:10197/d/AUTO_MAA/Server/apps_info.json", ) network.loop.exec() network_result = Network.get_result(network) @@ -2763,14 +2764,32 @@ class MemberManager(QWidget): content="选择一个保存路径,将当前配置信息导出到文件", parent=self, ) + self.card_ImportFromWeb = PushSettingCard( + text="查看", + icon=FluentIcon.PAGE_RIGHT, + title="从「AUTO_MAA 配置分享中心」导入", + content="从「AUTO_MAA 配置分享中心」选择一个用户分享的通用配置模板,导入其中的配置信息", + parent=self, + ) + self.card_UploadToWeb = PushSettingCard( + text="上传", + icon=FluentIcon.PAGE_RIGHT, + title="上传到「AUTO_MAA 配置分享中心」", + content="将当前通用配置分享到「AUTO_MAA 配置分享中心」,通过审核后可供其他用户下载使用", + parent=self, + ) self.card_ImportFromFile.clicked.connect(self.import_from_file) self.card_ExportToFile.clicked.connect(self.export_to_file) + self.card_ImportFromWeb.clicked.connect(self.import_from_web) + self.card_UploadToWeb.clicked.connect(self.upload_to_web) widget = QWidget() Layout = QVBoxLayout(widget) Layout.addWidget(self.card_ImportFromFile) Layout.addWidget(self.card_ExportToFile) + Layout.addWidget(self.card_ImportFromWeb) + Layout.addWidget(self.card_UploadToWeb) self.viewLayout.setContentsMargins(0, 0, 0, 0) self.viewLayout.setSpacing(0) self.addGroupWidget(widget) @@ -2791,6 +2810,16 @@ class MemberManager(QWidget): Config.member_dict[self.name]["Path"] / "config.json" ) + logger.success( + f"{self.name} 配置导入成功", module="脚本管理" + ) + MainInfoBar.push_info_bar( + "success", + "操作成功", + f"{self.name} 配置导入成功", + 3000, + ) + def export_to_file(self): """导出配置到文件""" @@ -2800,10 +2829,225 @@ class MemberManager(QWidget): if file_path: temp = self.config.toDict() + + # 移除配置中可能存在的隐私信息 temp["Script"]["Name"] = Path(file_path).stem + for path in ["ScriptPath", "ConfigPath", "LogPath"]: + + if Path(temp["Script"][path]).is_relative_to( + Path(temp["Script"]["RootPath"]) + ): + + temp["Script"][path] = str( + Path(r"C:/脚本根目录") + / Path(temp["Script"][path]).relative_to( + Path(temp["Script"]["RootPath"]) + ) + ) + temp["Script"]["RootPath"] = str(Path(r"C:/脚本根目录")) + with open(file_path, "w", encoding="utf-8") as file: json.dump(temp, file, ensure_ascii=False, indent=4) + logger.success( + f"{self.name} 配置导出成功", module="脚本管理" + ) + MainInfoBar.push_info_bar( + "success", + "操作成功", + f"{self.name} 配置导出成功", + 3000, + ) + + def import_from_web(self): + """从「AUTO_MAA 配置分享中心」导入配置""" + + # 从远程服务器获取配置列表 + network = Network.add_task( + mode="get", + url="http://221.236.27.82:10023/api/list/config/general", + ) + network.loop.exec() + network_result = Network.get_result(network) + if network_result["status_code"] == 200: + config_info: List[Dict[str, str]] = network_result[ + "response_json" + ] + else: + logger.warning( + f"获取配置列表时出错:{network_result['error_message']}", + module="脚本管理", + ) + MainInfoBar.push_info_bar( + "warning", + "获取配置列表时出错", + f"网络错误:{network_result['status_code']}", + 5000, + ) + return None + + choice = NoticeMessageBox( + self.window(), + "配置分享中心", + { + _[ + "configName" + ]: f""" +# {_['configName']} + +- **作者**: {_['author']} + +- **发布时间**:{_['createTime']} + +- **描述**:{_['description']} +""" + for _ in config_info + }, + ) + if choice.exec() and choice.currentIndex != 0: + + # 从远程服务器获取具体配置 + network = Network.add_task( + mode="get", + url=config_info[choice.currentIndex - 1]["downloadUrl"], + ) + network.loop.exec() + network_result = Network.get_result(network) + if network_result["status_code"] == 200: + config_data = network_result["response_json"] + else: + logger.warning( + f"获取配置列表时出错:{network_result['error_message']}", + module="脚本管理", + ) + MainInfoBar.push_info_bar( + "warning", + "获取配置列表时出错", + f"网络错误:{network_result['status_code']}", + 5000, + ) + return None + + with ( + Config.member_dict[self.name]["Path"] / "config.json" + ).open("w", encoding="utf-8") as file: + json.dump( + config_data, file, ensure_ascii=False, indent=4 + ) + self.config.load( + Config.member_dict[self.name]["Path"] / "config.json" + ) + + logger.success( + f"{self.name} 配置导入成功", module="脚本管理" + ) + MainInfoBar.push_info_bar( + "success", + "操作成功", + f"{self.name} 配置导入成功", + 3000, + ) + + def upload_to_web(self): + """上传配置到「AUTO_MAA 配置分享中心」""" + + choice = LineEditMessageBox( + self.window(), "请输入你的用户名", "用户名", "明文" + ) + choice.input.setMinimumWidth(200) + if choice.exec() and choice.input.text() != "": + + author = choice.input.text() + + choice = LineEditMessageBox( + self.window(), "请输入配置名称", "配置名称", "明文" + ) + choice.input.setMinimumWidth(200) + if choice.exec() and choice.input.text() != "": + + config_name = choice.input.text() + + choice = LineEditMessageBox( + self.window(), + "请描述一下您要分享的配置", + "配置描述", + "明文", + ) + choice.input.setMinimumWidth(300) + if choice.exec() and choice.input.text() != "": + + description = choice.input.text() + + temp = self.config.toDict() + + # 移除配置中可能存在的隐私信息 + temp["Script"]["Name"] = config_name + for path in ["ScriptPath", "ConfigPath", "LogPath"]: + if Path(temp["Script"][path]).is_relative_to( + Path(temp["Script"]["RootPath"]) + ): + temp["Script"][path] = str( + Path(r"C:/脚本根目录") + / Path( + temp["Script"][path] + ).relative_to( + Path(temp["Script"]["RootPath"]) + ) + ) + temp["Script"]["RootPath"] = str( + Path(r"C:/脚本根目录") + ) + + files = { + "file": ( + f"{config_name}&&{author}&&{description}&&{int(datetime.now().timestamp() * 1000)}.json", + json.dumps(temp, ensure_ascii=False), + "application/json", + ) + } + data = { + "username": author, + "description": description, + } + + # 配置上传至远程服务器 + network = Network.add_task( + "upload_file", + "http://221.236.27.82:10023/api/upload/share", + files=files, + data=data, + ) + network.loop.exec() + network_result = Network.get_result(network) + if network_result["status_code"] == 200: + response = network_result["response_json"] + else: + logger.warning( + f"上传配置时出错:{network_result['error_message']}", + module="脚本管理", + ) + MainInfoBar.push_info_bar( + "warning", + "上传配置时出错", + f"网络错误:{network_result['status_code']}", + 5000, + ) + return None + + logger.success( + f"{self.name} 配置上传成功", module="脚本管理" + ) + MainInfoBar.push_info_bar( + "success", + "上传配置成功", + ( + response["message"] + if "message" in response + else response["text"] + ), + 5000, + ) + class BranchManager(HeaderCardWidget): """分支管理父页面""" diff --git a/app/ui/setting.py b/app/ui/setting.py index cada615..0f93be1 100644 --- a/app/ui/setting.py +++ b/app/ui/setting.py @@ -473,7 +473,7 @@ class Setting(QWidget): # 从远程服务器获取代理信息 network = Network.add_task( mode="get", - url="https://gitee.com/DLmaster_361/AUTO_MAA/raw/server/download_info.json", + url="http://221.236.27.82:10197/d/AUTO_MAA/Server/download_info.json", ) network.loop.exec() network_result = Network.get_result(network) @@ -565,7 +565,7 @@ class Setting(QWidget): # 从远程服务器获取最新公告 network = Network.add_task( mode="get", - url="https://gitee.com/DLmaster_361/AUTO_MAA/raw/server/notice.json", + url="http://221.236.27.82:10197/d/AUTO_MAA/Server/notice.json", ) network.loop.exec() network_result = Network.get_result(network) diff --git a/main.py b/main.py index 576caf2..a682ae8 100644 --- a/main.py +++ b/main.py @@ -26,6 +26,10 @@ v4.4 """ +import os +import sys + + # Nuitka环境检测和修复 def setup_nuitka_compatibility(): """设置Nuitka打包环境的兼容性""" @@ -84,8 +88,6 @@ def no_print(*args, **kwargs): builtins.print = no_print -import os -import sys import ctypes import traceback from PySide6.QtWidgets import QApplication diff --git a/resources/version.json b/resources/version.json index 60ba22e..d4e596c 100644 --- a/resources/version.json +++ b/resources/version.json @@ -2,9 +2,15 @@ "main_version": "4.4.1.2", "version_info": { "4.4.1.2": { + "新增功能": [ + "AUTO_MAA 配置分享中心上线" + ], "修复BUG": [ "日志读取添加兜底机制", "修复 QTimer.singleShot 参数问题" + ], + "程序优化": [ + "小文件配置信息转移至AUTO_MAA自建服务" ] }, "4.4.1.1": {