feat(bootstrap): 自动更新可禁用
This commit is contained in:
parent
b377b8445e
commit
3f88c3a6c4
|
@ -1,16 +1,17 @@
|
|||
import importlib.metadata
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import json
|
||||
import ctypes
|
||||
import codecs
|
||||
import locale
|
||||
from typing import Optional, Dict, Any
|
||||
import logging
|
||||
import subprocess
|
||||
import importlib.metadata
|
||||
from pathlib import Path
|
||||
from collections import deque
|
||||
from datetime import datetime
|
||||
from time import sleep
|
||||
from typing import Optional, Dict, Any, TypedDict, Literal, List
|
||||
|
||||
from request import head, HTTPError, NetworkError
|
||||
from terminal import (
|
||||
|
@ -18,6 +19,29 @@ from terminal import (
|
|||
get_terminal_width, get_display_width, truncate_string,
|
||||
hide_cursor, show_cursor, move_cursor_up, wait_key, get_terminal_height
|
||||
)
|
||||
from repo import Version
|
||||
|
||||
# 配置文件的类型定义
|
||||
class BackendConfig(TypedDict, total=False):
|
||||
type: Literal['custom', 'mumu12', 'leidian', 'dmm']
|
||||
screenshot_impl: Literal['adb', 'adb_raw', 'uiautomator2', 'windows', 'remote_windows', 'nemu_ipc']
|
||||
|
||||
class MiscConfig(TypedDict, total=False):
|
||||
check_update: Literal['never', 'startup']
|
||||
auto_install_update: bool
|
||||
|
||||
class UserConfig(TypedDict, total=False):
|
||||
name: str
|
||||
id: str
|
||||
category: str
|
||||
description: str
|
||||
backend: BackendConfig
|
||||
keep_screenshots: bool
|
||||
options: Dict[str, Any] # 这里包含 misc 等配置
|
||||
|
||||
class Config(TypedDict, total=False):
|
||||
version: int
|
||||
user_configs: List[UserConfig]
|
||||
|
||||
# 获取当前Python解释器路径
|
||||
python_executable = sys.executable
|
||||
|
@ -296,40 +320,123 @@ def run_command(command: str, check: bool = True, verbatim: bool = False, scroll
|
|||
|
||||
return success
|
||||
|
||||
def install_pip_and_ksaa(pip_server: str) -> bool:
|
||||
def check_ksaa_update_available(pip_server: str, current_version: Version) -> tuple[bool, Version | None, Version | None]:
|
||||
"""
|
||||
检查ksaa包是否有新版本可用。
|
||||
|
||||
:param pip_server: pip服务器URL
|
||||
:type pip_server: str
|
||||
:param current_version: 当前版本
|
||||
:type current_version: Version
|
||||
:return: (是否有更新, 当前版本, 最新版本)
|
||||
:rtype: tuple[bool, Optional[Version], Optional[Version]]
|
||||
"""
|
||||
try:
|
||||
# 使用repo.py中的list_versions函数和Version类获取最新版本信息
|
||||
from repo import list_versions, Version
|
||||
|
||||
try:
|
||||
versions = list_versions("ksaa", server_url=pip_server)
|
||||
if versions and len(versions) > 0:
|
||||
latest_version = versions[0].version
|
||||
|
||||
# 使用Version类的比较功能
|
||||
if latest_version > current_version:
|
||||
return True, current_version, latest_version
|
||||
except Exception as e:
|
||||
logging.warning(f"从服务器 {pip_server} 获取版本信息失败: {e}")
|
||||
print_status(f"从服务器 {pip_server} 获取版本信息失败: {e}", status='error')
|
||||
# 如果指定服务器失败,尝试使用默认PyPI服务器
|
||||
try:
|
||||
versions = list_versions("ksaa")
|
||||
if versions and len(versions) > 0:
|
||||
latest_version = versions[0].version
|
||||
|
||||
# 使用Version类的比较功能
|
||||
if latest_version > current_version:
|
||||
return True, current_version, latest_version
|
||||
except Exception as e2:
|
||||
logging.warning(f"从PyPI获取版本信息也失败: {e2}")
|
||||
|
||||
return False, current_version, latest_version if 'latest_version' in locals() else None
|
||||
|
||||
except Exception as e:
|
||||
logging.warning(f"检查ksaa更新时发生错误: {e}")
|
||||
return False, None, None
|
||||
|
||||
def print_update_notice(current_version: str, latest_version: str):
|
||||
"""
|
||||
打印更新提示信息。
|
||||
|
||||
:param current_version: 当前版本
|
||||
:type current_version: str
|
||||
:param latest_version: 最新版本
|
||||
:type latest_version: str
|
||||
"""
|
||||
clear_screen()
|
||||
print()
|
||||
print(f"{Color.YELLOW}{Color.BOLD}" + "=" * 60)
|
||||
print(f"{Color.YELLOW}{Color.BOLD}⚠️ 发现新版本可用!")
|
||||
print(f"{Color.YELLOW}{Color.BOLD}" + "=" * 60)
|
||||
print(f"{Color.YELLOW}当前版本: {current_version}")
|
||||
print(f"{Color.YELLOW}最新版本: {latest_version}")
|
||||
print(f"{Color.YELLOW}建议开启自动更新或在设置中手动安装新版本。")
|
||||
print(f"{Color.YELLOW}5s 后继续启动")
|
||||
print(f"{Color.YELLOW}{Color.BOLD}" + "=" * 60 + f"{Color.RESET}")
|
||||
print()
|
||||
sleep(5)
|
||||
|
||||
def install_pip_and_ksaa(pip_server: str, check_update: bool = True, install_update: bool = True) -> bool:
|
||||
"""
|
||||
安装和更新pip以及ksaa包。
|
||||
|
||||
:param pip_server: pip服务器URL
|
||||
:type pip_server: str
|
||||
:param check_update: 是否检查更新
|
||||
:type check_update: bool
|
||||
:param install_update: 是否安装更新
|
||||
:type install_update: bool
|
||||
:return: 安装是否成功
|
||||
:rtype: bool
|
||||
"""
|
||||
print_header("安装与更新依赖", color=Color.BLUE)
|
||||
print_header("安装与更新小助手", color=Color.BLUE)
|
||||
|
||||
# 定义信任的主机列表
|
||||
trusted_hosts = "pypi.org files.pythonhosted.org pypi.python.org mirrors.aliyun.com mirrors.cloud.tencent.com mirrors.tuna.tsinghua.edu.cn"
|
||||
|
||||
# 升级pip
|
||||
print_status("检查并更新 pip", status='info')
|
||||
print_status("更新 pip", status='info')
|
||||
upgrade_pip_command = f'"{python_executable}" -m pip install -i {pip_server} --trusted-host "{trusted_hosts}" --upgrade pip'
|
||||
if not run_command(upgrade_pip_command):
|
||||
return False
|
||||
|
||||
# 安装ksaa,通过命令行参数传递配置
|
||||
print_status("安装或更新 ksaa", status='info')
|
||||
install_command = f'"{python_executable}" -m pip install --upgrade --index-url {pip_server} --trusted-host "{trusted_hosts}" ksaa'
|
||||
if not run_command(install_command):
|
||||
return False
|
||||
|
||||
install_command = f'"{python_executable}" -m pip install --upgrade --index-url {pip_server} --trusted-host "{trusted_hosts}" --no-warn-script-location ksaa'
|
||||
ksaa_version_str = package_version("ksaa")
|
||||
# 未安装
|
||||
if not ksaa_version_str:
|
||||
print_status("安装琴音小助手", status='info')
|
||||
return run_command(install_command)
|
||||
# 已安装,检查更新
|
||||
else:
|
||||
ksaa_version = Version(ksaa_version_str)
|
||||
if check_update:
|
||||
has_update, current_version, latest_version = check_ksaa_update_available(pip_server, ksaa_version)
|
||||
if has_update:
|
||||
if install_update:
|
||||
print_status("更新琴音小助手", status='info')
|
||||
return run_command(install_command)
|
||||
else:
|
||||
print_update_notice(str(current_version), str(latest_version))
|
||||
else:
|
||||
print_status("已是最新版本", status='success')
|
||||
return True
|
||||
|
||||
def load_config() -> Optional[Dict[str, Any]]:
|
||||
def load_config() -> Optional[Config]:
|
||||
"""
|
||||
加载config.json配置文件。
|
||||
|
||||
:return: 配置字典,如果加载失败返回None
|
||||
:rtype: Optional[Dict[str, Any]]
|
||||
:rtype: Optional[Config]
|
||||
"""
|
||||
config_path = Path("./config.json")
|
||||
if not config_path.exists():
|
||||
|
@ -351,6 +458,38 @@ def load_config() -> Optional[Dict[str, Any]]:
|
|||
logging.error(msg, exc_info=True)
|
||||
return None
|
||||
|
||||
def get_update_settings(config: Config) -> tuple[bool, bool]:
|
||||
"""
|
||||
从配置中获取更新设置。
|
||||
|
||||
:param config: 配置字典
|
||||
:type config: Config
|
||||
:return: (是否检查更新, 是否自动安装更新)
|
||||
:rtype: tuple[bool, bool]
|
||||
"""
|
||||
# 默认值
|
||||
check_update = True
|
||||
auto_install_update = True
|
||||
|
||||
# 检查是否有用户配置
|
||||
user_configs = config.get("user_configs", [])
|
||||
if user_configs:
|
||||
first_config = user_configs[0]
|
||||
options = first_config.get("options", {})
|
||||
misc = options.get("misc", {})
|
||||
|
||||
# 获取检查更新设置
|
||||
check_update_setting = misc.get("check_update", "startup")
|
||||
check_update = check_update_setting == "startup"
|
||||
|
||||
# 获取自动安装更新设置
|
||||
auto_install_update = misc.get("auto_install_update", True)
|
||||
|
||||
msg = f"更新设置: 检查更新={check_update}, 自动安装={auto_install_update}"
|
||||
logging.info(msg)
|
||||
|
||||
return check_update, auto_install_update
|
||||
|
||||
def restart_as_admin() -> None:
|
||||
"""
|
||||
以管理员身份重启程序。
|
||||
|
@ -382,24 +521,25 @@ def restart_as_admin() -> None:
|
|||
logging.error(msg, exc_info=True)
|
||||
return
|
||||
|
||||
def check_admin(config: Dict[str, Any]) -> bool:
|
||||
def check_admin(config: Config) -> bool:
|
||||
"""
|
||||
检查Windows截图权限(管理员权限)。
|
||||
|
||||
:param config: 配置字典
|
||||
:type config: Dict[str, Any]
|
||||
:type config: Config
|
||||
:return: 权限检查是否通过
|
||||
:rtype: bool
|
||||
"""
|
||||
# 检查是否有用户配置
|
||||
if not config.get("user_configs"):
|
||||
user_configs = config.get("user_configs", [])
|
||||
if not user_configs:
|
||||
msg = "配置文件中没有用户配置"
|
||||
print_status(msg, status='warning')
|
||||
logging.warning(msg)
|
||||
return True # Not a fatal error, allow to continue
|
||||
|
||||
# 检查第一个用户配置的截图方式
|
||||
first_config = config["user_configs"][0]
|
||||
first_config = user_configs[0]
|
||||
backend = first_config.get("backend", {})
|
||||
screenshot_impl = backend.get("screenshot_impl")
|
||||
|
||||
|
@ -457,27 +597,31 @@ def main_launch():
|
|||
logging.info("启动器已启动。")
|
||||
|
||||
try:
|
||||
# 1. 获取可用的pip服务器
|
||||
# 1. 加载配置文件(提前加载以获取更新设置)
|
||||
print_header("加载配置", color=Color.BLUE)
|
||||
logging.info("加载配置。")
|
||||
config = load_config()
|
||||
|
||||
# 2. 获取更新设置
|
||||
check_update, auto_install_update = get_update_settings(config if config else {"version": 5, "user_configs": []})
|
||||
|
||||
# 3. 根据配置决定是否检查更新
|
||||
print_status("正在寻找最快的 PyPI 镜像源...", status='info')
|
||||
logging.info("正在寻找最快的 PyPI 镜像源...")
|
||||
pip_server = get_working_pip_server()
|
||||
if not pip_server:
|
||||
raise RuntimeError("没有找到可用的pip服务器,请检查网络连接。")
|
||||
|
||||
# 2. 安装和更新pip以及ksaa包
|
||||
if not install_pip_and_ksaa(pip_server):
|
||||
# 4. 安装和更新pip以及ksaa包
|
||||
if not install_pip_and_ksaa(pip_server, check_update, auto_install_update):
|
||||
raise RuntimeError("依赖安装失败,请检查上面的错误日志。")
|
||||
|
||||
# 3. 加载配置文件
|
||||
print_header("加载配置", color=Color.BLUE)
|
||||
logging.info("加载配置。")
|
||||
config = load_config()
|
||||
# 5. 检查Windows截图权限
|
||||
if config:
|
||||
# 4. 检查Windows截图权限
|
||||
if not check_admin(config):
|
||||
raise RuntimeError("权限检查失败。")
|
||||
|
||||
# 5. 运行KAA
|
||||
# 6. 运行KAA
|
||||
if not run_kaa():
|
||||
raise RuntimeError("KAA 主程序运行失败。")
|
||||
|
||||
|
|
|
@ -78,6 +78,8 @@ class Version:
|
|||
def __repr__(self):
|
||||
return f"Version('{self.version_str}')"
|
||||
|
||||
def __str__(self):
|
||||
return self.version_str
|
||||
|
||||
@dataclass
|
||||
class PackageVersion:
|
||||
|
|
3
justfile
3
justfile
|
@ -125,6 +125,7 @@ publish-test: package
|
|||
#
|
||||
build-bootstrap:
|
||||
#!{{shebang_pwsh}}
|
||||
echo "Building bootstrap"...
|
||||
# 构建 Python
|
||||
cd bootstrap
|
||||
python -m zipapp kaa-bootstrap
|
||||
|
@ -134,7 +135,7 @@ build-bootstrap:
|
|||
$msbuild = &"${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -prerelease -products * -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe
|
||||
if ($msbuild) {
|
||||
& $msbuild kaa-wrapper/kaa-wrapper.sln /p:Configuration=Release
|
||||
mv kaa-wrapper/x64/Release/kaa-wrapper.exe ../dist/bootstrap.exe -fo
|
||||
mv kaa-wrapper/x64/Release/kaa-wrapper.exe ../dist/kaa.exe -fo
|
||||
} else {
|
||||
Write-Host "MSBuild not found. Please install Visual Studio or build kaa-wrapper manually."
|
||||
}
|
|
@ -461,6 +461,21 @@ class EndGameConfig(ConfigBaseModel):
|
|||
(目前仅对 DMM 版有效。)
|
||||
"""
|
||||
|
||||
class MiscConfig(ConfigBaseModel):
|
||||
check_update: Literal['never', 'startup'] = 'startup'
|
||||
"""
|
||||
检查更新时机。
|
||||
|
||||
* never: 从不检查更新。
|
||||
* startup: 启动时检查更新。
|
||||
"""
|
||||
auto_install_update: bool = True
|
||||
"""
|
||||
是否自动安装更新。
|
||||
|
||||
若启用,则每次自动检查更新时若有新版本会自动安装,否则只是会提示。
|
||||
"""
|
||||
|
||||
class BaseConfig(ConfigBaseModel):
|
||||
purchase: PurchaseConfig = PurchaseConfig()
|
||||
"""商店购买配置"""
|
||||
|
@ -501,6 +516,9 @@ class BaseConfig(ConfigBaseModel):
|
|||
end_game: EndGameConfig = EndGameConfig()
|
||||
"""关闭游戏配置"""
|
||||
|
||||
misc: MiscConfig = MiscConfig()
|
||||
"""杂项配置"""
|
||||
|
||||
|
||||
def conf() -> BaseConfig:
|
||||
"""获取当前配置数据"""
|
||||
|
|
|
@ -22,7 +22,7 @@ from kotonebot.kaa.common import (
|
|||
BaseConfig, APShopItems, CapsuleToysConfig, ClubRewardConfig, PurchaseConfig, ActivityFundsConfig,
|
||||
PresentsConfig, AssignmentConfig, ContestConfig, ProduceConfig,
|
||||
MissionRewardConfig, DailyMoneyShopItems, ProduceAction,
|
||||
RecommendCardDetectionMode, TraceConfig, StartGameConfig, EndGameConfig, UpgradeSupportCardConfig,
|
||||
RecommendCardDetectionMode, TraceConfig, StartGameConfig, EndGameConfig, UpgradeSupportCardConfig, MiscConfig,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -88,6 +88,9 @@ ConfigKey = Literal[
|
|||
'presents_enabled',
|
||||
'trace_recommend_card_detection',
|
||||
|
||||
# misc
|
||||
'check_update', 'auto_install_update',
|
||||
|
||||
'_selected_backend_index'
|
||||
|
||||
]
|
||||
|
@ -1469,6 +1472,35 @@ class KotoneBotUI:
|
|||
'trace_recommend_card_detection': trace_recommend_card_detection
|
||||
}
|
||||
|
||||
def _create_misc_settings(self) -> ConfigBuilderReturnValue:
|
||||
with gr.Column():
|
||||
gr.Markdown("### 杂项设置")
|
||||
check_update = gr.Dropdown(
|
||||
choices=[
|
||||
("启动时检查更新", "startup"),
|
||||
("从不检查更新", "never")
|
||||
],
|
||||
value=self.current_config.options.misc.check_update,
|
||||
label="检查更新时机",
|
||||
info=MiscConfig.model_fields['check_update'].description,
|
||||
interactive=True
|
||||
)
|
||||
auto_install_update = gr.Checkbox(
|
||||
label="自动安装更新",
|
||||
value=self.current_config.options.misc.auto_install_update,
|
||||
info=MiscConfig.model_fields['auto_install_update'].description,
|
||||
interactive=True
|
||||
)
|
||||
|
||||
def set_config(config: BaseConfig, data: dict[ConfigKey, Any]) -> None:
|
||||
config.misc.check_update = data['check_update']
|
||||
config.misc.auto_install_update = data['auto_install_update']
|
||||
|
||||
return set_config, {
|
||||
'check_update': check_update,
|
||||
'auto_install_update': auto_install_update
|
||||
}
|
||||
|
||||
def _create_settings_tab(self) -> None:
|
||||
with gr.Tab("设置"):
|
||||
gr.Markdown("## 设置")
|
||||
|
@ -1506,6 +1538,9 @@ class KotoneBotUI:
|
|||
# 跟踪设置
|
||||
trace_settings = self._create_trace_settings()
|
||||
|
||||
# 杂项设置
|
||||
misc_settings = self._create_misc_settings()
|
||||
|
||||
# 启动游戏设置
|
||||
start_game_settings = self._create_start_game_settings()
|
||||
|
||||
|
@ -1529,7 +1564,8 @@ class KotoneBotUI:
|
|||
capsule_toys_settings,
|
||||
start_game_settings,
|
||||
end_game_settings,
|
||||
trace_settings
|
||||
trace_settings,
|
||||
misc_settings
|
||||
] # list of (set_func, { 'key': component, ... })
|
||||
all_components = [list(ret[1].values()) for ret in all_return_values] # [[c1, c2], [c3], ...]
|
||||
all_components = list(chain(*all_components)) # [c1, c2, c3, ...]
|
||||
|
|
Loading…
Reference in New Issue