Compare commits

..

No commits in common. "main" and "dev" have entirely different histories.
main ... dev

29 changed files with 119 additions and 423 deletions

View file

@ -2,29 +2,18 @@
## 开发环境 ## 开发环境
安装 `pip-tools` 目前只有 `pywebview` 一个依赖。
```bash
pip install pip-tools
```
安装依赖
```bash
pip-sync requirements.txt
```
## 打包 ## 打包
前端运行 `npm run build` 生成 `ui/dist`,之后安装 PyInstaller,运行 前端运行 `npm run build` 生成 `ui/dist`,之后安装 PyInstaller,运行
```bash ```bash
pyinstaller -w --add-data "ui/dist:ui/dist" --add-data "launcher/sys_config/config_dist.json:launcher/sys_config" launcher.py pyinstaller -w --add-data "ui/dist:ui/dist" --add-data "launcher/sys_config/config_dist.yaml:launcher/sys_config" launcher.py
``` ```
在dist文件夹生成launcher文件夹 在dist文件夹生成launcher文件夹
```bash ```bash
cd dist tar -cf dist/launcher.tar dist/launcher
py7zr c launcher.7z launcher
``` ```

View file

@ -1,8 +1,8 @@
import mimetypes import mimetypes
import shutil import os
from pathlib import Path from pathlib import Path
from launcher.constants import update_tmp_folder from launcher.constants import upgrade_script_name
from launcher.webview import start_webview from launcher.webview import start_webview
mimetypes.add_type("text/html", ".html") mimetypes.add_type("text/html", ".html")
@ -11,8 +11,8 @@ mimetypes.add_type("application/javascript", ".js")
if __name__ == '__main__': if __name__ == '__main__':
# 如果当前路径存在临时文件夹,则删除 # 如果当前路径存在更新脚本,则删除
if Path(update_tmp_folder).exists(): if Path(upgrade_script_name).exists():
shutil.rmtree(update_tmp_folder) os.remove(upgrade_script_name)
start_webview() start_webview()

View file

@ -1,15 +1,26 @@
import json import json
import os import os
from pathlib import Path
import yaml
from yamlcore import CoreDumper, CoreLoader
from launcher.config.conf import Conf from launcher.config.conf import Conf
from pathlib import Path
conf_path = Path(os.path.join(os.getcwd(), "conf.json")) conf_path = Path(os.path.join(os.getcwd(), "conf.json"))
def save_conf(): def save_conf():
with conf_path.open("w", encoding="utf8") as f: with conf_path.open("w", encoding="utf8") as f:
json.dump(conf.model_dump(), f, ensure_ascii=False, indent=4) json.dump(conf.model_dump(), f, ensure_ascii=False, indent=4) # Use json.dump
# yaml.dump(
# conf.model_dump(),
# f,
# Dumper=CoreDumper,
# encoding="utf-8",
# default_flow_style=False,
# allow_unicode=True,
# )
def load_conf(): def load_conf():
@ -19,8 +30,8 @@ def load_conf():
conf = Conf() conf = Conf()
save_conf() save_conf()
return return
with conf_path.open("r", encoding="utf-8") as file: with conf_path.open("r", encoding="utf-8") as f:
data = json.load(file) data = yaml.load(f, Loader=CoreLoader)
if data is None: if data is None:
data = {} data = {}
conf = Conf(**data) conf = Conf(**data)

View file

@ -19,8 +19,6 @@ class Total(ConfModel):
"""整体""" """整体"""
# 所在页面 # 所在页面
page: str = "init" page: str = "init"
# 是否已展示帮助文档
is_already_show_doc: bool = False
class UpdatePart(ConfModel): class UpdatePart(ConfModel):

View file

@ -4,8 +4,6 @@ constants.py
该模块定义了应用程序中使用的各种常量这些常量包括API URL和其他配置参数 该模块定义了应用程序中使用的各种常量这些常量包括API URL和其他配置参数
""" """
# 更新临时文件夹名
update_tmp_folder = "download_tmp"
# 更新脚本名 # 更新脚本名
upgrade_script_name = "upgrade.bat" upgrade_script_name = "upgrade.bat"
@ -23,3 +21,4 @@ mirror_list = {
"tuna": "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple", "tuna": "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple",
"sjtu": "https://mirror.sjtu.edu.cn/pypi/web/simple", "sjtu": "https://mirror.sjtu.edu.cn/pypi/web/simple",
} }

View file

@ -5,7 +5,7 @@ import requests
from launcher.file.extract import extract_7z_file from launcher.file.extract import extract_7z_file
from launcher.file.utils import format_size from launcher.file.utils import format_size
from launcher.webview.events import custom_event, LogType from launcher.webview.events import custom_event
def download_file(download_name, download_url, destination_folder): def download_file(download_name, download_url, destination_folder):
@ -21,7 +21,7 @@ def download_file(download_name, download_url, destination_folder):
filename = os.path.basename(download_url) filename = os.path.basename(download_url)
download_path = os.path.join(destination_folder, filename) download_path = os.path.join(destination_folder, filename)
custom_event(LogType.info, f"开始下载: {download_name}") custom_event(f"开始下载: {download_name}")
response = requests.get(download_url, stream=True) response = requests.get(download_url, stream=True)
if response.status_code == 200: if response.status_code == 200:
@ -52,7 +52,7 @@ def download_file(download_name, download_url, destination_folder):
formatted_total_size = format_size(total_size) formatted_total_size = format_size(total_size)
formatted_speed = format_size(download_speed) + "/s" formatted_speed = format_size(download_speed) + "/s"
custom_event(LogType.info, custom_event(
f"下载进度: {progress_percent:.2f}% ({formatted_downloaded_size}/{formatted_total_size}), 下载速度: {formatted_speed}") f"下载进度: {progress_percent:.2f}% ({formatted_downloaded_size}/{formatted_total_size}), 下载速度: {formatted_speed}")
last_update_time = current_time # 更新上次更新时间 last_update_time = current_time # 更新上次更新时间
@ -63,10 +63,10 @@ def download_file(download_name, download_url, destination_folder):
formatted_total_elapsed_time = f"{total_elapsed_time:.2f}" formatted_total_elapsed_time = f"{total_elapsed_time:.2f}"
formatted_average_download_speed = format_size(average_download_speed) + "/s" formatted_average_download_speed = format_size(average_download_speed) + "/s"
custom_event(LogType.info, custom_event(
f"下载完成: {filename}, 耗时: {formatted_total_elapsed_time}, 下载速度: {formatted_average_download_speed}") f"下载完成: {filename}, 耗时: {formatted_total_elapsed_time}, 下载速度: {formatted_average_download_speed}")
else: else:
custom_event(LogType.error, f"下载失败: {response.status_code}") custom_event(f"下载失败: {response.status_code}")
return False return False
return True return True
@ -84,7 +84,7 @@ def init_download(download_name, download_url, destination_folder):
def download(): def download():
target_folder = os.path.join(download_name) target_folder = os.path.join(download_name)
if os.path.exists(target_folder): if os.path.exists(target_folder):
custom_event(LogType.info, f"{download_name} 文件夹已存在,跳过下载") custom_event(f"{download_name} 文件夹已存在,跳过下载")
return True return True
filename = os.path.basename(download_url) filename = os.path.basename(download_url)

View file

@ -5,7 +5,7 @@ from py7zr import py7zr
from py7zr.callbacks import ExtractCallback from py7zr.callbacks import ExtractCallback
from launcher.file.utils import format_size from launcher.file.utils import format_size
from launcher.webview.events import custom_event, LogType from launcher.webview.events import custom_event
class MyExtractCallback(ExtractCallback): class MyExtractCallback(ExtractCallback):
@ -24,7 +24,7 @@ class MyExtractCallback(ExtractCallback):
self.total_size += int(wrote_bytes) self.total_size += int(wrote_bytes)
current_time = time.time() current_time = time.time()
if current_time - self.last_print_time >= 1.0: # 至少每隔1秒输出一次 if current_time - self.last_print_time >= 1.0: # 至少每隔1秒输出一次
custom_event(LogType.info, f"已解压: {format_size(self.total_size)}") custom_event(f"已解压: {format_size(self.total_size)}")
self.last_print_time = current_time self.last_print_time = current_time
def report_postprocess(self): def report_postprocess(self):
@ -49,7 +49,7 @@ def extract_7z_file(file_name, file_path, destination_folder, delete_after_extra
if not os.path.exists(destination_folder): if not os.path.exists(destination_folder):
os.makedirs(destination_folder) os.makedirs(destination_folder)
custom_event(LogType.info, f"开始解压文件: {file_name}") custom_event(f"开始解压文件: {file_name}")
try: try:
start_time = time.time() start_time = time.time()
with py7zr.SevenZipFile(file_path, mode='r') as z: with py7zr.SevenZipFile(file_path, mode='r') as z:
@ -58,17 +58,18 @@ def extract_7z_file(file_name, file_path, destination_folder, delete_after_extra
end_time = time.time() end_time = time.time()
total_elapsed_time = end_time - start_time total_elapsed_time = end_time - start_time
formatted_total_elapsed_time = f"{total_elapsed_time:.2f}" formatted_total_elapsed_time = f"{total_elapsed_time:.2f}"
custom_event(LogType.info, custom_event(
f"解压完成: {file_name}, 总大小: {format_size(callback.total_size)}, 耗时: {formatted_total_elapsed_time}") f"解压完成: {file_name}, 总大小: {format_size(callback.total_size)}, 耗时: {formatted_total_elapsed_time}")
except Exception as e: except Exception as e:
custom_event(LogType.error, f"解压失败: {repr(e)}") e.print_exc()
custom_event(f"解压失败: {str(e)}")
return False return False
if delete_after_extract: if delete_after_extract:
try: try:
os.remove(file_path) os.remove(file_path)
custom_event(LogType.info, f"删除{file_path}成功") custom_event(f"删除{file_path}成功")
except OSError as e: except OSError as e:
custom_event(LogType.error, f"删除{file_path}失败: {repr(e)}") custom_event(f"删除{file_path}失败: {str(e)}")
return True return True

View file

@ -1,6 +1,3 @@
import os
def format_size(size_bytes): def format_size(size_bytes):
"""格式化文件大小为人类可读的形式""" """格式化文件大小为人类可读的形式"""
for unit in ['B', 'KB', 'MB', 'GB', 'TB']: for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
@ -8,23 +5,3 @@ def format_size(size_bytes):
return f"{size_bytes:.2f} {unit}" return f"{size_bytes:.2f} {unit}"
size_bytes /= 1024 size_bytes /= 1024
return f"{size_bytes:.2f} PB" return f"{size_bytes:.2f} PB"
def ensure_directory_exists(directory):
"""确保目录存在,如果不存在则创建"""
if not os.path.exists(directory):
os.makedirs(directory)
def check_command_path(command, cwd=None):
"""检查命令路径是否存在exe文件
:return 是否存在,命令exe文件全路径
"""
command_path = command.split(" ")[0]
if command_path == "start":
return True, None
exec_command_path = os.getcwd()
if cwd:
exec_command_path = os.path.join(exec_command_path, cwd)
full_command_path = os.path.abspath(os.path.join(exec_command_path, command_path)) + ".exe"
return os.path.exists(full_command_path), full_command_path

View file

@ -1,6 +1,5 @@
import io import io
import logging import logging
import os
import sys import sys
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
@ -22,9 +21,8 @@ def setup_logger():
console_handler.setLevel(log_level) console_handler.setLevel(log_level)
# 文件输出 # 文件输出
file_path = os.path.join(os.getcwd(), "launcher.log")
file_handler = RotatingFileHandler( file_handler = RotatingFileHandler(
file_path, maxBytes=5 * 1024 * 1024, backupCount=3, encoding='utf-8' "../launcher.log", maxBytes=5 * 1024 * 1024, backupCount=3, encoding='utf-8'
) )
file_handler.setLevel(logging.INFO) file_handler.setLevel(logging.INFO)

View file

@ -1,6 +1,7 @@
import json
import os
import sys import sys
import os
import yaml
class SysConfig: class SysConfig:
@ -13,8 +14,6 @@ class SysConfig:
url: str url: str
# 日志输出级别 # 日志输出级别
log_level: str log_level: str
# 是否开启ui调试
debug: bool
def __init__(self): def __init__(self):
self.config = {} self.config = {}
@ -27,13 +26,13 @@ class SysConfig:
# 如果是打包后的可执行文件 # 如果是打包后的可执行文件
base_path = sys._MEIPASS base_path = sys._MEIPASS
config_subdir = 'launcher/sys_config' # 添加子目录 config_subdir = 'launcher/sys_config' # 添加子目录
config_filename = 'config_dist.json' config_filename = 'config_dist.yaml'
else: else:
# logger.error("本地配置") # logger.error("本地配置")
# 如果是本地开发环境 # 如果是本地开发环境
base_path = os.path.dirname(__file__) base_path = os.path.dirname(__file__)
config_subdir = '' # 本地开发环境不需要子目录 config_subdir = '' # 本地开发环境不需要子目录
config_filename = 'config_local.json' config_filename = 'config_local.yaml'
config_path = os.path.join(base_path, config_subdir, config_filename) config_path = os.path.join(base_path, config_subdir, config_filename)
return config_path return config_path
@ -41,13 +40,13 @@ class SysConfig:
def load_config(self): def load_config(self):
try: try:
with open(self.config_path, 'r', encoding='utf-8') as file: with open(self.config_path, 'r', encoding='utf-8') as file:
self.config = json.load(file) self.config = yaml.safe_load(file)
except FileNotFoundError: except FileNotFoundError:
pass pass
# logger.error(f"配置文件未找到: {self.config_path}") # logger.error(f"配置文件未找到: {self.config_path}")
except Exception: except Exception as e:
pass pass
# logger.error(f"加载配置文件时出错: {repr(e)}") # logger.error(f"加载配置文件时出错: {str(e)}")
def get(self, key): def get(self, key):
return self.config.get(key) return self.config.get(key)

View file

@ -1,6 +0,0 @@
{
"version": "v0.5",
"url": "ui/dist/index.html",
"log_level": "ERROR",
"debug": false
}

View file

@ -0,0 +1,3 @@
version: "v0.3"
url: "ui/dist/index.html"
log_level: "ERROR"

View file

@ -1,6 +0,0 @@
{
"version": "dev",
"url": "http://localhost:5173/",
"log_level": "INFO",
"debug": true
}

View file

@ -0,0 +1,3 @@
version: "dev"
url: "http://localhost:5173/"
log_level: "INFO"

View file

@ -1,13 +1,12 @@
import webview import webview
from launcher.sys_config import sys_config
from launcher.webview.api import Api from launcher.webview.api import Api
from launcher.sys_config import sys_config
window = None window = None
def start_webview(): def start_webview():
global window global window
window = webview.create_window(f"mower-ng launcher {sys_config.get('version')}", sys_config.get('url'), window = webview.create_window(f"mower-ng launcher {sys_config.get('version')}", sys_config.get('url'), js_api=Api())
js_api=Api(), width=850, height=600) webview.start()
webview.start(debug=sys_config.get('debug'))

View file

@ -7,17 +7,16 @@ from pathlib import Path
from shutil import rmtree from shutil import rmtree
from subprocess import Popen from subprocess import Popen
import chardet
import requests import requests
from launcher import config from launcher import config
from launcher.constants import download_git_url, download_python_url, get_new_version_url, upgrade_script_name, \ from launcher.constants import download_git_url, download_python_url, get_new_version_url, upgrade_script_name, \
mirror_list mirror_list
from launcher.file.download import init_download, download_file from launcher.file.download import init_download, download_file
from launcher.file.extract import extract_7z_file
from launcher.file.utils import ensure_directory_exists, check_command_path
from launcher.log import logger from launcher.log import logger
from launcher.sys_config import sys_config from launcher.sys_config import sys_config
from launcher.webview.events import custom_event, LogType from launcher.webview.events import custom_event
command_list = { command_list = {
"download_git": lambda: init_download("git", download_git_url, os.getcwd()), "download_git": lambda: init_download("git", download_git_url, os.getcwd()),
@ -28,43 +27,27 @@ command_list = {
"fetch": lambda: f"..\\git\\bin\\git fetch origin {config.conf.branch} --progress", "fetch": lambda: f"..\\git\\bin\\git fetch origin {config.conf.branch} --progress",
"switch": lambda: f"..\\git\\bin\\git -c lfs.concurrenttransfers=100 switch -f {config.conf.branch} --progress", "switch": lambda: f"..\\git\\bin\\git -c lfs.concurrenttransfers=100 switch -f {config.conf.branch} --progress",
"reset": lambda: f"..\\git\\bin\\git -c lfs.concurrenttransfers=200 reset --hard origin/{config.conf.branch}", "reset": lambda: f"..\\git\\bin\\git -c lfs.concurrenttransfers=200 reset --hard origin/{config.conf.branch}",
"pip_tools_install": lambda: f"..\\python\\Scripts\\pip install --no-cache-dir -i {mirror_list[config.conf.mirror]} pip-tools --no-warn-script-location", "pip_install": lambda: f"..\\python\\Scripts\\pip install --no-cache-dir -i {mirror_list[config.conf.mirror]} -r requirements.txt --no-warn-script-location",
"pip_sync": lambda: f"..\\python\\Scripts\\pip-sync -i {mirror_list[config.conf.mirror]} requirements.txt",
"webview": "start ..\\python\\pythonw webview_ui.py", "webview": "start ..\\python\\pythonw webview_ui.py",
"manager": "start ..\\python\\pythonw manager.py", "manager": "start ..\\python\\pythonw manager.py",
} }
def parse_stderr(stderr_output): def detect_encoding(stream):
error_keywords = { # 读取一部分数据来检测编码
"fatal: destination path 'mower-ng' already exists and is not an empty directory.": "mower-ng文件夹已存在并且非空。", raw_data = stream.read(4096)
"index.lock': File exists": "上一个git命令正在执行,请等待执行结束或在任务管理器中杀掉git进程,并确保上方提示的index.lock文件删除后再次运行。", result = chardet.detect(raw_data)
"Could not resolve host": "网络出现错误,请检查网络是否通畅。", stream.seek(0) # 将流指针重置到开头
"ReadTimeoutError": "网络连接超时,请检查网络连接或尝试更换镜像源。", return result['encoding']
"No space left on device": "磁盘空间不足。",
}
for keyword, message in error_keywords.items():
if keyword in stderr_output:
return message
return "未定义的错误"
def read_stream(stream, log_type, output_list=None): def read_stream(stream, log_func):
def process_lines(text_io): detected_encoding = detect_encoding(stream)
for line in iter(text_io.readline, ''):
text = line.rstrip('\n').strip()
custom_event(log_type, text)
if output_list is not None:
output_list.append(text)
detected_encoding = 'utf-8'
text_io = io.TextIOWrapper(stream, encoding=detected_encoding, errors='replace') text_io = io.TextIOWrapper(stream, encoding=detected_encoding, errors='replace')
try: try:
process_lines(text_io) for line in iter(text_io.readline, ''):
except UnicodeDecodeError: text = line.rstrip('\n')
stream.seek(0) # 重新将流指针重置到开头 custom_event(text.strip() + "\n")
text_io = io.TextIOWrapper(stream, encoding='gbk', errors='replace')
process_lines(text_io)
finally: finally:
text_io.close() text_io.close()
@ -91,33 +74,25 @@ class Api:
# 更新启动器本身 # 更新启动器本身
def update_self(self, download_url): def update_self(self, download_url):
logger.info(f"开始更新启动器 {download_url}") logger.info(f"开始更新启动器 {download_url}")
# 下载压缩包的全路径
file_name = os.path.basename(download_url) file_name = os.path.basename(download_url)
file_name = "launcher.7z"
current_path = os.getcwd() current_path = os.getcwd()
download_tmp_folder = os.path.join(current_path, "download_tmp") if not download_file("launcher", download_url, current_path):
# 确保 download_tmp 文件夹存在
ensure_directory_exists(download_tmp_folder)
if not download_file("launcher", download_url, download_tmp_folder):
return "下载新版本失败" return "下载新版本失败"
download_path = os.path.join(download_tmp_folder, file_name) download_path = os.path.join(current_path, file_name)
if not extract_7z_file("launcher", download_path, download_tmp_folder, True): script_path = os.path.join(os.getcwd(), upgrade_script_name)
return "解压新版本失败" folder_path = os.path.join(os.getcwd(), "_internal")
exe_path = os.path.join(os.getcwd(), "launcher.exe")
exe_path = os.path.join(current_path, "launcher.exe")
folder_path = os.path.join(current_path, "_internal")
new_exe_path = os.path.join(download_tmp_folder, "launcher", "launcher.exe")
new_folder_path = os.path.join(download_tmp_folder, "launcher", "_internal")
script_path = os.path.join(download_tmp_folder, upgrade_script_name)
with open(script_path, "w") as b: with open(script_path, "w") as b:
temp_list = "@echo off\n" temp_list = "@echo off\n"
temp_list += "timeout /t 3 /nobreak\n" # 等待进程退出 temp_list += "timeout /t 3 /nobreak\n" # 等待进程退出
temp_list += f"rmdir {folder_path}\n" # 删除_internal temp_list += f"rmdir {folder_path}\n" # 删除_internal
temp_list += f"del {exe_path}\n" # 删除exe temp_list += f"del {exe_path}\n" # 删除exe
temp_list += f"xcopy /e /i /y {new_folder_path} {folder_path}\n" temp_list += f"tar -xf {download_path} -C ..\n" # 解压压缩包
temp_list += f"move {new_exe_path} {exe_path}\n" temp_list += "timeout /t 1 /nobreak\n" # 等待解压
temp_list += "timeout /t 1 /nobreak\n" # 等待操作完成
temp_list += f"start {exe_path}\n" # 启动新程序 temp_list += f"start {exe_path}\n" # 启动新程序
temp_list += f"del {download_path}\n" # 删除压缩包
temp_list += "exit" temp_list += "exit"
b.write(temp_list) b.write(temp_list)
# 不显示cmd窗口 # 不显示cmd窗口
@ -125,24 +100,18 @@ class Api:
os._exit(0) os._exit(0)
def rm_site_packages(self): def rm_site_packages(self):
try:
site_packages_path = Path("./python/Lib/site-packages") site_packages_path = Path("./python/Lib/site-packages")
if site_packages_path.exists(): if site_packages_path.exists():
rmtree(site_packages_path) rmtree(site_packages_path)
return "site-packages目录移除成功" return "site-packages目录移除成功"
return "python\\Lib\\site-packages目录不存在" return "python\\Lib\\site-packages目录不存在"
except Exception as e:
return repr(e)
def rm_python_scripts(self): def rm_python_scripts(self):
try:
python_scripts_path = Path("./python/Scripts") python_scripts_path = Path("./python/Scripts")
if python_scripts_path.exists(): if python_scripts_path.exists():
rmtree(python_scripts_path) rmtree(python_scripts_path)
return "Scripts目录移除成功" return "Scripts目录移除成功"
return "python\\Scripts目录不存在" return "python\\Scripts目录不存在"
except Exception as e:
return repr(e)
def run(self, command, cwd=None): def run(self, command, cwd=None):
command = command_list[command] command = command_list[command]
@ -150,37 +119,23 @@ class Api:
command = command() command = command()
if callable(command): if callable(command):
return "success" if command() else "failed" return "success" if command() else "failed"
if cwd is not None: custom_event(command + "\n")
custom_event(LogType.info, f"命令执行目录:{cwd}")
custom_event(LogType.execute_command, command)
# 执行命令前先判断命令路径是否存在
exist, command_path = check_command_path(command, cwd)
if not exist:
custom_event(LogType.error, f"命令路径不存在:{command_path} 请尝试依赖修复并重新初始化。")
return "failed"
try: try:
stdout_stderr = []
with subprocess.Popen( with subprocess.Popen(
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, cwd=cwd, bufsize=0, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, cwd=cwd, bufsize=0,
universal_newlines=False universal_newlines=False
) as p: ) as p:
stdout_thread = threading.Thread(target=read_stream, args=(p.stdout, LogType.command_out)) stdout_thread = threading.Thread(target=read_stream, args=(p.stdout, logger.info))
stderr_thread = threading.Thread(target=read_stream, stderr_thread = threading.Thread(target=read_stream, args=(p.stderr, logger.info))
args=(p.stderr, LogType.command_out, stdout_stderr))
stdout_thread.start() stdout_thread.start()
stderr_thread.start() stderr_thread.start()
stdout_thread.join() stdout_thread.join()
stderr_thread.join() stderr_thread.join()
if p.returncode == 0: if p.returncode == 0:
return "success" return "success"
else:
error_message = parse_stderr("\n".join(stdout_stderr))
custom_event(LogType.error, f"命令执行失败,{error_message}")
return "failed"
except Exception as e: except Exception as e:
logger.exception(e) logger.exception(e)
custom_event(LogType.error, str(e)) custom_event(str(e))
return "failed" return "failed"

View file

@ -1,32 +1,11 @@
import json import json
from enum import Enum
import launcher import launcher
from launcher.log import logger from launcher.log import logger
class LogType(Enum): def custom_event(data):
info = "信息"
error = "错误"
execute_command = "执行命令"
command_out = "命令输出"
def custom_event(log_type, data):
data = f"[{log_type.value}] {data}"
match log_type:
case LogType.info:
logger.info(data) logger.info(data)
case LogType.error:
logger.error(data)
case LogType.execute_command:
logger.info(data)
case LogType.command_out:
logger.info(data)
case _:
logger.info(data)
data = json.dumps({"log": data + "\n"}) data = json.dumps({"log": data + "\n"})
js = f"var event = new CustomEvent('log', {{detail: {data}}}); window.dispatchEvent(event);" js = f"var event = new CustomEvent('log', {{detail: {data}}}); window.dispatchEvent(event);"
launcher.webview.window.evaluate_js(js) launcher.webview.window.evaluate_js(js)

View file

@ -1,5 +0,0 @@
pywebview==5.1
requests==2.32.3
py7zr==0.22.0
pydantic==2.10.3
pyinstaller==6.11.1

View file

@ -1,80 +0,0 @@
#
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# pip-compile requirements.in
#
--index-url https://pypi.tuna.tsinghua.edu.cn/simple
altgraph==0.17.4
# via pyinstaller
annotated-types==0.7.0
# via pydantic
bottle==0.13.2
# via pywebview
brotli==1.1.0
# via py7zr
certifi==2024.12.14
# via requests
cffi==1.17.1
# via clr-loader
charset-normalizer==3.4.0
# via requests
clr-loader==0.2.7.post0
# via pythonnet
idna==3.10
# via requests
inflate64==1.0.0
# via py7zr
multivolumefile==0.2.3
# via py7zr
packaging==24.2
# via
# pyinstaller
# pyinstaller-hooks-contrib
pefile==2023.2.7
# via pyinstaller
proxy-tools==0.1.0
# via pywebview
psutil==6.1.0
# via py7zr
py7zr==0.22.0
# via -r requirements.in
pybcj==1.0.2
# via py7zr
pycparser==2.22
# via cffi
pycryptodomex==3.21.0
# via py7zr
pydantic==2.10.3
# via -r requirements.in
pydantic-core==2.27.1
# via pydantic
pyinstaller==6.11.1
# via -r requirements.in
pyinstaller-hooks-contrib==2024.11
# via pyinstaller
pyppmd==1.1.0
# via py7zr
pythonnet==3.0.5
# via pywebview
pywebview==5.1
# via -r requirements.in
pywin32-ctypes==0.2.3
# via pyinstaller
pyzstd==0.16.2
# via py7zr
requests==2.32.3
# via -r requirements.in
texttable==1.7.0
# via py7zr
typing-extensions==4.12.2
# via
# pydantic
# pydantic-core
# pywebview
urllib3==2.2.3
# via requests
# The following packages are considered to be unsafe in a requirements file:
# setuptools

9
ui/package-lock.json generated
View file

@ -8,7 +8,6 @@
"name": "ui", "name": "ui",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"highlight.js": "^11.11.1",
"pinia": "^2.2.8", "pinia": "^2.2.8",
"vue": "^3.5.11" "vue": "^3.5.11"
}, },
@ -2076,9 +2075,11 @@
} }
}, },
"node_modules/highlight.js": { "node_modules/highlight.js": {
"version": "11.11.1", "version": "11.10.0",
"resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.10.0.tgz",
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", "integrity": "sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==",
"dev": true,
"license": "BSD-3-Clause",
"engines": { "engines": {
"node": ">=12.0.0" "node": ">=12.0.0"
} }

View file

@ -11,7 +11,6 @@
"format": "prettier --write src/" "format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
"highlight.js": "^11.11.1",
"pinia": "^2.2.8", "pinia": "^2.2.8",
"vue": "^3.5.11" "vue": "^3.5.11"
}, },

View file

@ -12,10 +12,6 @@ const loading = ref(true)
const page = ref(null) const page = ref(null)
let conf let conf
function show_doc() {
window.open('https://hedgedoc.zhaozuohong.vip/s/LfSzK2n0K', '_blank')
}
async function init_version() { async function init_version() {
version.value = await pywebview.api.get_version() version.value = await pywebview.api.get_version()
new_version.value = await pywebview.api.get_new_version() new_version.value = await pywebview.api.get_new_version()
@ -26,12 +22,8 @@ async function init_version() {
async function initialize_config() { async function initialize_config() {
await configStore.load_config() await configStore.load_config()
conf = configStore.config conf = configStore.config
loading.value = false
if (!conf.is_already_show_doc) {
show_doc()
conf.is_already_show_doc = true
}
await init_version() await init_version()
loading.value = false
} }
const log = ref('') const log = ref('')
@ -89,7 +81,6 @@ provide('new_version', new_version)
class="container" class="container"
v-model:value="conf.page" v-model:value="conf.page"
@update:value="set_page" @update:value="set_page"
justify-content="center"
> >
<n-tab-pane :disabled="running" name="init" tab="初始化"><init /></n-tab-pane> <n-tab-pane :disabled="running" name="init" tab="初始化"><init /></n-tab-pane>
<n-tab-pane :disabled="running" name="update" tab="更新代码"><update /></n-tab-pane> <n-tab-pane :disabled="running" name="update" tab="更新代码"><update /></n-tab-pane>
@ -97,18 +88,13 @@ provide('new_version', new_version)
<n-tab-pane :disabled="running" name="fix" tab="依赖修复"><fix /></n-tab-pane> <n-tab-pane :disabled="running" name="fix" tab="依赖修复"><fix /></n-tab-pane>
<n-tab-pane :disabled="running" name="settings"> <n-tab-pane :disabled="running" name="settings">
<template #tab> <template #tab>
<div class="tab-content"> <n-space :wrap="false">
<span>设置</span> 设置
<n-tag v-if="update_able" class="tag" round type="success"></n-tag> <n-tag v-if="update_able" round type="success"></n-tag>
</div> </n-space>
</template> </template>
<settings /> <settings />
</n-tab-pane> </n-tab-pane>
<template #suffix>
<div class="suffix-container">
<n-button type="primary" secondary size="small" @click="show_doc">帮助文档</n-button>
</div>
</template>
</n-tabs> </n-tabs>
</n-notification-provider> </n-notification-provider>
<n-global-style /> <n-global-style />
@ -120,17 +106,4 @@ provide('new_version', new_version)
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
} }
.suffix-container {
margin: 0 4px 6px 4px;
}
.tab-content {
position: relative;
}
.tag {
margin-left: 4px;
position: absolute;
top: 50%;
left: 100%;
transform: translateY(-50%);
}
</style> </style>

View file

@ -1,57 +1,14 @@
<script setup> <script setup>
import { darkTheme } from 'naive-ui' import { darkTheme } from 'naive-ui'
import hljs from 'highlight.js/lib/core'
const log = inject('log') const log = inject('log')
const log_ele = inject('log_ele') const log_ele = inject('log_ele')
const chinesePattern = {
className: 'chinese',
begin: /[\u4e00-\u9fa5]+/
}
hljs.registerLanguage('naive-log', () => ({
contains: [
{
className: 'info',
begin: /^\[信息\]/,
end: /$/,
returnBegin: true,
returnEnd: true,
contains: [chinesePattern]
},
{
className: 'error',
begin: /^\[错误\]/,
end: /$/,
returnBegin: true,
returnEnd: true,
contains: [chinesePattern]
},
{
className: 'execute_command',
begin: /^\[执行命令\]/,
end: /$/,
returnBegin: true,
returnEnd: true,
contains: [chinesePattern]
},
{
className: 'command_out',
begin: /^\[命令输出\]/,
end: /$/,
returnBegin: true,
returnEnd: true,
contains: [chinesePattern]
}
]
}))
</script> </script>
<template> <template>
<n-config-provider :theme="darkTheme" class="provider" :hljs="hljs"> <n-config-provider :theme="darkTheme" class="provider">
<n-card class="full" content-style="height: 100%"> <n-card class="full" content-style="height: 100%">
<n-log :log="log" class="full selectable-log" ref="log_ele" language="naive-log" /> <n-log :log="log" class="full" ref="log_ele" />
</n-card> </n-card>
</n-config-provider> </n-config-provider>
</template> </template>
@ -66,8 +23,10 @@ hljs.registerLanguage('naive-log', () => ({
height: 100% !important; height: 100% !important;
box-sizing: border-box; box-sizing: border-box;
} }
</style>
.selectable-log { <style>
user-select: text; pre {
word-break: break-all !important;
} }
</style> </style>

View file

@ -1,6 +1,5 @@
import 'vfonts/Lato.css' import 'vfonts/Lato.css'
import 'vfonts/FiraCode.css' import 'vfonts/FiraCode.css'
import './styles/global.css'
import { createApp } from 'vue' import { createApp } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'

View file

@ -1,6 +1,5 @@
<script setup> <script setup>
import { SyncCircle } from '@vicons/ionicons5' import { SyncCircle } from '@vicons/ionicons5'
import { form_item_label_style } from '@/styles/styles.js'
const notification = useNotification() const notification = useNotification()
@ -58,7 +57,7 @@ async function check_update() {
<template> <template>
<n-flex vertical style="gap: 16px; height: 100%; padding: 16px; box-sizing: border-box"> <n-flex vertical style="gap: 16px; height: 100%; padding: 16px; box-sizing: border-box">
<n-form label-placement="left" :show-feedback="false" label-width="auto" label-align="left"> <n-form label-placement="left" :show-feedback="false" label-width="auto" label-align="left">
<n-form-item label="版本" :label-style="form_item_label_style"> <n-form-item label="版本">
<n-space align="center"> <n-space align="center">
{{ version }} {{ version }}
<n-button <n-button

View file

@ -1,6 +1,5 @@
<script setup> <script setup>
import { useConfigStore } from '@/stores/config.js' import { useConfigStore } from '@/stores/config.js'
import { form_item_label_style } from '@/styles/styles.js'
const conf = useConfigStore().config const conf = useConfigStore().config
const branch = ref(null) const branch = ref(null)
@ -14,7 +13,7 @@ const steps = computed(() => [
}, },
{ {
title: '安装依赖', title: '安装依赖',
command: ['pip_tools_install', 'pip_sync'], command: ['pip_install'],
cwd: 'mower-ng' cwd: 'mower-ng'
} }
]) ])
@ -28,7 +27,7 @@ provide('current_state', current_state)
<template> <template>
<n-flex vertical style="gap: 16px; height: 100%; padding: 16px; box-sizing: border-box"> <n-flex vertical style="gap: 16px; height: 100%; padding: 16px; box-sizing: border-box">
<n-form label-placement="left" :show-feedback="false" label-width="auto" label-align="left"> <n-form label-placement="left" :show-feedback="false" label-width="auto" label-align="left">
<n-form-item label="mower-ng 代码分支" :label-style="form_item_label_style"> <n-form-item label="mower-ng 代码分支">
<n-radio-group v-model:value="conf.branch"> <n-radio-group v-model:value="conf.branch">
<n-flex> <n-flex>
<n-radio value="fast">测试版</n-radio> <n-radio value="fast">测试版</n-radio>
@ -36,7 +35,7 @@ provide('current_state', current_state)
</n-flex> </n-flex>
</n-radio-group> </n-radio-group>
</n-form-item> </n-form-item>
<n-form-item label="PyPI 仓库镜像" :label-style="form_item_label_style"> <n-form-item label="PyPI 仓库镜像">
<n-radio-group v-model:value="conf.mirror"> <n-radio-group v-model:value="conf.mirror">
<n-flex> <n-flex>
<n-radio value="pypi">PyPI</n-radio> <n-radio value="pypi">PyPI</n-radio>

View file

@ -6,7 +6,6 @@ export const useConfigStore = defineStore('config', () => {
this.page = conf.page this.page = conf.page
this.branch = conf.branch this.branch = conf.branch
this.mirror = conf.mirror this.mirror = conf.mirror
this.is_already_show_doc = conf.is_already_show_doc
} }
} }

View file

@ -1,43 +0,0 @@
/* src/assets/styles/global.css */
/* 日志样式 */
.n-code pre {
word-break: break-all !important;
}
.n-code pre .hljs-info {
color: #33ff33;
}
.n-code pre .hljs-error {
color: #ff0000;
}
.n-code pre .hljs-execute_command {
color: #edaf1f;
}
.n-code pre .hljs-command_out {
color: #ffffff;
}
.n-code pre .hljs-chinese {
font-family: '微软雅黑', sans-serif;
}
/* 确保 chinese 类在所有父级类下生效 */
.n-code pre .hljs-info .hljs-chinese {
color: #33ff33;
}
.n-code pre .hljs-error .hljs-chinese {
color: #ff0000;
}
.n-code pre .hljs-execute_command .hljs-chinese {
color: #edaf1f;
}
.n-code pre .hljs-command_out .hljs-chinese {
color: #ffffff;
}

View file

@ -1,3 +0,0 @@
export const form_item_label_style = {
alignSelf: 'center'
}