feat(core): 增强离线模式支持和版本管理

- 在主窗口中添加离线模式管理器,支持自动切换到离线模式。
- 更新下载管理器以处理离线模式下的下载逻辑,确保用户体验流畅。
- 添加版本警告机制,提示用户在版本过低时的操作选项。
- 优化配置管理器,确保在离线模式下仍可使用相关功能。
- 更新UI管理器以反映当前工作模式,提升用户界面友好性。
This commit is contained in:
hyb-oyqq
2025-08-06 15:22:44 +08:00
parent b18f4a276c
commit 7befe19f30
17 changed files with 1707 additions and 148 deletions

7
.gitignore vendored
View File

@@ -173,4 +173,9 @@ cython_debug/
nuitka-crash-report.xml
build.bat
log.txt
result.csv
result.csv
after.7z
vol.1.7z
vol.2.7z
vol.3.7z
vol.4.7z

View File

@@ -85,16 +85,34 @@ class ConfigManager:
# 记录错误信息,用于按钮点击时显示
if error_message == "update_required":
self.last_error_message = "update_required"
msg_box = msgbox_frame(
f"更新提示 - {self.app_name}",
"\n当前版本过低,请及时更新。\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
# 在浏览器中打开项目主页
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/")
# 版本过低,应当显示"无法安装"
return {"action": "disable_button", "then": "exit"}
# 检查是否处于离线模式
is_offline_mode = False
if hasattr(self.debug_manager, 'main_window') and hasattr(self.debug_manager.main_window, 'offline_mode_manager'):
is_offline_mode = self.debug_manager.main_window.offline_mode_manager.is_in_offline_mode()
if is_offline_mode:
# 离线模式下只显示提示,不禁用开始安装按钮
msg_box = msgbox_frame(
f"更新提示 - {self.app_name}",
"\n当前版本过低,请及时更新。\n在离线模式下,您仍可使用禁用/启用补丁、卸载补丁和离线安装功能。\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
# 移除在浏览器中打开项目主页的代码
# 离线模式下版本过低,仍然允许使用安装按钮
return {"action": "enable_button"}
else:
# 在线模式下显示强制更新提示
msg_box = msgbox_frame(
f"更新提示 - {self.app_name}",
"\n当前版本过低,请及时更新。\n如需联网下载补丁,请更新到最新版,否则无法下载。\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
# 移除在浏览器中打开项目主页的代码
# 在线模式下版本过低,但不直接禁用按钮,而是在点击时提示
return {"action": "enable_button", "version_warning": True}
elif "missing_keys" in error_message:
self.last_error_message = "missing_keys"
@@ -128,8 +146,8 @@ class ConfigManager:
)
msg_box.exec()
# 网络错误时应当显示"无法安装"
return {"action": "disable_button"}
# 网络错误时仍然允许使用按钮,用户可以尝试离线模式
return {"action": "enable_button"}
else:
self.cloud_config = data
# 标记配置有效
@@ -139,10 +157,36 @@ class ConfigManager:
if debug_mode:
print("--- Cloud config fetched successfully ---")
print(json.dumps(data, indent=2))
# 创建一个数据副本隐藏敏感URL
safe_data = self._create_safe_config_for_logging(data)
print(json.dumps(safe_data, indent=2))
# 获取配置成功,允许安装
return {"action": "enable_button"}
def _create_safe_config_for_logging(self, config_data):
"""创建用于日志记录的安全配置副本隐藏敏感URL
Args:
config_data: 原始配置数据
Returns:
dict: 安全的配置数据副本
"""
if not config_data or not isinstance(config_data, dict):
return config_data
# 创建深拷贝,避免修改原始数据
import copy
safe_config = copy.deepcopy(config_data)
# 隐藏敏感URL
for key in safe_config:
if isinstance(safe_config[key], dict) and "url" in safe_config[key]:
# 完全隐藏URL
safe_config[key]["url"] = "***URL protection***"
return safe_config
def is_config_valid(self):
"""检查配置是否有效

View File

@@ -31,9 +31,20 @@ class DebugManager:
Returns:
bool: 是否处于调试模式
"""
if hasattr(self, 'ui_manager') and hasattr(self.ui_manager, 'debug_action'):
return self.ui_manager.debug_action.isChecked()
return False
try:
# 首先尝试从UI管理器获取状态
if hasattr(self, 'ui_manager') and self.ui_manager and hasattr(self.ui_manager, 'debug_action') and self.ui_manager.debug_action:
return self.ui_manager.debug_action.isChecked()
# 如果UI管理器还没准备好尝试从配置中获取
if hasattr(self.main_window, 'config') and isinstance(self.main_window.config, dict):
return self.main_window.config.get('debug_mode', False)
# 如果以上都不可行返回False
return False
except Exception:
# 捕获任何异常默认返回False
return False
def toggle_debug_mode(self, checked):
"""切换调试模式
@@ -51,6 +62,21 @@ class DebugManager:
if checked:
self.start_logging()
# 如果启用了调试模式,检查是否需要强制启用离线模式
if hasattr(self.main_window, 'offline_mode_manager'):
# 检查配置中是否已设置离线模式
offline_mode_enabled = self.main_window.config.get("offline_mode", False)
# 如果配置中已设置离线模式,则在调试模式下强制启用
if offline_mode_enabled:
print("DEBUG: 调试模式下强制启用离线模式")
self.main_window.offline_mode_manager.set_offline_mode(True)
# 更新UI中的离线模式选项
if hasattr(self.ui_manager, 'offline_mode_action') and self.ui_manager.offline_mode_action:
self.ui_manager.offline_mode_action.setChecked(True)
self.ui_manager.online_mode_action.setChecked(False)
else:
self.stop_logging()

View File

@@ -100,7 +100,9 @@ class DownloadManager:
raise ValueError("未能获取或解析配置数据")
if self.is_debug_mode():
print(f"DEBUG: Parsed JSON data: {json.dumps(config_data, indent=2)}")
# 创建安全版本的配置数据用于调试输出
safe_config = self._create_safe_config_for_logging(config_data)
print(f"DEBUG: Parsed JSON data: {json.dumps(safe_config, indent=2)}")
urls = {}
for i in range(4):
@@ -125,7 +127,18 @@ class DownloadManager:
raise ValueError(f"配置文件缺少必要的键: {', '.join(missing_original_keys)}")
if self.is_debug_mode():
print(f"DEBUG: Extracted URLs: {urls}")
# 创建安全版本的URL字典用于调试输出
safe_urls = {}
for key, url in urls.items():
# 保留域名部分,隐藏路径
import re
domain_match = re.match(r'(https?://[^/]+)/.*', url)
if domain_match:
domain = domain_match.group(1)
safe_urls[key] = f"{domain}/***隐藏URL路径***"
else:
safe_urls[key] = "***隐藏URL***"
print(f"DEBUG: Extracted URLs: {safe_urls}")
print("--- Finished getting download URL successfully ---")
return urls
@@ -158,11 +171,41 @@ class DownloadManager:
f"\n配置文件格式异常\n\n【错误信息】:{e}\n",
)
return {}
def _create_safe_config_for_logging(self, config_data):
"""创建用于日志记录的安全配置副本隐藏敏感URL
Args:
config_data: 原始配置数据
Returns:
dict: 安全的配置数据副本
"""
if not config_data or not isinstance(config_data, dict):
return config_data
# 创建深拷贝,避免修改原始数据
import copy
safe_config = copy.deepcopy(config_data)
# 隐藏敏感URL
for key in safe_config:
if isinstance(safe_config[key], dict) and "url" in safe_config[key]:
# 完全隐藏URL
safe_config[key]["url"] = "***URL protection***"
return safe_config
def download_action(self):
"""开始下载流程"""
self.main_window.download_queue_history = []
# 清除游戏检测器的目录缓存,确保获取最新的目录状态
if hasattr(self.main_window, 'game_detector') and hasattr(self.main_window.game_detector, 'clear_directory_cache'):
self.main_window.game_detector.clear_directory_cache()
if self.is_debug_mode():
print("DEBUG: 已清除游戏目录缓存,确保获取最新状态")
game_dirs = self.main_window.game_detector.identify_game_directories_improved(self.selected_folder)
debug_mode = self.is_debug_mode()
@@ -182,7 +225,7 @@ class DownloadManager:
self.main_window.ui.start_install_text.setText("开始安装")
return
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="pre")
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="pre", is_offline=False)
install_paths = self.get_install_paths()
@@ -345,22 +388,38 @@ class DownloadManager:
self.main_window.setEnabled(False)
config = self.get_download_url()
if not config:
QtWidgets.QMessageBox.critical(
self.main_window, f"错误 - {APP_NAME}", "\n网络状态异常或服务器故障,请重试\n"
)
self.main_window.setEnabled(True)
self.main_window.ui.start_install_text.setText("开始安装")
return
# 检查是否处于离线模式
is_offline_mode = False
if hasattr(self.main_window, 'offline_mode_manager'):
is_offline_mode = self.main_window.offline_mode_manager.is_in_offline_mode()
if is_offline_mode:
if debug_mode:
print("DEBUG: 使用离线模式,跳过网络配置获取")
self._fill_offline_download_queue(selected_game_dirs)
else:
config = self.get_download_url()
if not config:
QtWidgets.QMessageBox.critical(
self.main_window, f"错误 - {APP_NAME}", "\n网络状态异常或服务器故障,请重试\n"
)
self.main_window.setEnabled(True)
self.main_window.ui.start_install_text.setText("开始安装")
return
self._fill_download_queue(config, selected_game_dirs)
self._fill_download_queue(config, selected_game_dirs)
if not self.download_queue:
self.main_window.after_hash_compare()
return
self._show_cloudflare_option()
# 如果是离线模式,直接开始下一个下载任务
if is_offline_mode:
if debug_mode:
print("DEBUG: 离线模式跳过Cloudflare优化")
self.next_download_task()
else:
self._show_cloudflare_option()
def _fill_download_queue(self, config, game_dirs):
"""填充下载队列
@@ -406,6 +465,68 @@ class DownloadManager:
self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path))
self.main_window.download_queue_history.append(game_version)
def _fill_offline_download_queue(self, game_dirs):
"""填充离线模式下的下载队列
Args:
game_dirs: 包含游戏文件夹路径的字典
"""
self.download_queue.clear()
if not hasattr(self.main_window, 'download_queue_history'):
self.main_window.download_queue_history = []
debug_mode = self.is_debug_mode()
if debug_mode:
print(f"DEBUG: 填充离线下载队列, 游戏目录: {game_dirs}")
# 检查是否有离线模式管理器
if not hasattr(self.main_window, 'offline_mode_manager'):
if debug_mode:
print("DEBUG: 离线模式管理器未初始化,无法使用离线模式")
return
for i in range(1, 5):
game_version = f"NEKOPARA Vol.{i}"
if game_version in game_dirs and not self.main_window.installed_status.get(game_version, False):
# 获取离线补丁文件路径
offline_patch_path = self.main_window.offline_mode_manager.get_offline_patch_path(game_version)
if not offline_patch_path:
if debug_mode:
print(f"DEBUG: 未找到 {game_version} 的离线补丁文件,跳过")
continue
game_folder = game_dirs[game_version]
if debug_mode:
print(f"DEBUG: 使用识别到的游戏目录 {game_version}: {game_folder}")
print(f"DEBUG: 使用离线补丁文件: {offline_patch_path}")
_7z_path = os.path.join(PLUGIN, f"vol.{i}.7z")
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
# 将本地文件路径作为URL添加到下载队列
self.download_queue.append((offline_patch_path, game_folder, game_version, _7z_path, plugin_path))
self.main_window.download_queue_history.append(game_version)
game_version = "NEKOPARA After"
if game_version in game_dirs and not self.main_window.installed_status.get(game_version, False):
# 获取离线补丁文件路径
offline_patch_path = self.main_window.offline_mode_manager.get_offline_patch_path(game_version)
if offline_patch_path:
game_folder = game_dirs[game_version]
if debug_mode:
print(f"DEBUG: 使用识别到的游戏目录 {game_version}: {game_folder}")
print(f"DEBUG: 使用离线补丁文件: {offline_patch_path}")
_7z_path = os.path.join(PLUGIN, "after.7z")
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
# 将本地文件路径作为URL添加到下载队列
self.download_queue.append((offline_patch_path, game_folder, game_version, _7z_path, plugin_path))
self.main_window.download_queue_history.append(game_version)
elif debug_mode:
print(f"DEBUG: 未找到 {game_version} 的离线补丁文件,跳过")
def _show_cloudflare_option(self):
"""显示Cloudflare加速选择对话框"""
if self.download_queue:
@@ -498,7 +619,7 @@ class DownloadManager:
"""准备下载特定游戏版本
Args:
url: 下载URL
url: 下载URL或本地文件路径
game_folder: 游戏文件夹路径
game_version: 游戏版本名称
_7z_path: 7z文件保存路径
@@ -511,6 +632,10 @@ class DownloadManager:
print(f"DEBUG: 准备下载游戏 {game_version}")
print(f"DEBUG: 游戏文件夹: {game_folder}")
# 隐藏敏感URL
safe_url = "***URL protection***" # 完全隐藏URL
print(f"DEBUG: 下载URL: {safe_url}")
game_exe_exists = True
if (
@@ -525,15 +650,76 @@ class DownloadManager:
self.next_download_task()
return
self.main_window.progress_window = self.main_window.create_progress_window()
# 检查是否处于离线模式
is_offline_mode = False
if hasattr(self.main_window, 'offline_mode_manager'):
is_offline_mode = self.main_window.offline_mode_manager.is_in_offline_mode()
self.optimized_ip = self.cloudflare_optimizer.get_optimized_ip()
if self.optimized_ip:
print(f"已为 {game_version} 获取到优选IP: {self.optimized_ip}")
# 如果是离线模式且URL是本地文件路径
if is_offline_mode and os.path.isfile(url):
if debug_mode:
print(f"DEBUG: 离线模式,复制本地补丁文件 {url}{_7z_path}")
try:
# 确保目标目录存在
os.makedirs(os.path.dirname(_7z_path), exist_ok=True)
# 复制文件
import shutil
shutil.copy2(url, _7z_path)
# 验证文件哈希
hash_valid = False
if hasattr(self.main_window, 'offline_mode_manager'):
if debug_mode:
print(f"DEBUG: 开始验证补丁文件哈希: {_7z_path}")
hash_valid = self.main_window.offline_mode_manager.verify_patch_hash(game_version, _7z_path)
if debug_mode:
print(f"DEBUG: 补丁文件哈希验证结果: {'成功' if hash_valid else '失败'}")
else:
if debug_mode:
print("DEBUG: 离线模式管理器不可用,跳过哈希验证")
hash_valid = True # 如果没有离线模式管理器,假设验证成功
if hash_valid:
if debug_mode:
print(f"DEBUG: 成功复制并验证补丁文件 {_7z_path}")
# 直接进入解压阶段
self.main_window.extraction_handler.start_extraction(_7z_path, game_folder, plugin_path, game_version)
self.main_window.extraction_handler.extraction_finished.connect(self.on_extraction_finished)
else:
if debug_mode:
print(f"DEBUG: 补丁文件哈希验证失败")
# 显示错误消息
QtWidgets.QMessageBox.critical(
self.main_window,
f"错误 - {APP_NAME}",
f"\n补丁文件校验失败: {game_version}\n\n文件可能已损坏或被篡改,请重新获取补丁文件。\n"
)
# 继续下一个任务
self.next_download_task()
except Exception as e:
if debug_mode:
print(f"DEBUG: 复制补丁文件失败: {e}")
# 显示错误消息
QtWidgets.QMessageBox.critical(
self.main_window,
f"错误 - {APP_NAME}",
f"\n复制补丁文件失败: {game_version}\n错误: {e}\n"
)
# 继续下一个任务
self.next_download_task()
else:
print(f"未能为 {game_version} 获取优选IP将使用默认线路。")
# 在线模式,正常下载
self.main_window.progress_window = self.main_window.create_progress_window()
self.optimized_ip = self.cloudflare_optimizer.get_optimized_ip()
if self.optimized_ip:
print(f"已为 {game_version} 获取到优选IP: {self.optimized_ip}")
else:
print(f"未能为 {game_version} 获取优选IP将使用默认线路。")
self.download_task_manager.start_download(url, _7z_path, game_version, game_folder, plugin_path)
self.download_task_manager.start_download(url, _7z_path, game_version, game_folder, plugin_path)
def on_download_finished(self, success, error, url, game_folder, game_version, _7z_path, plugin_path):
"""下载完成后的处理

View File

@@ -24,8 +24,16 @@ class ExtractionHandler:
plugin_path: 插件路径
game_version: 游戏版本名称
"""
# 检查是否处于离线模式
is_offline = False
if hasattr(self.main_window, 'offline_mode_manager'):
is_offline = self.main_window.offline_mode_manager.is_in_offline_mode()
# 显示解压中的消息窗口
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="extraction")
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(
check_type="offline_extraction" if is_offline else "extraction",
is_offline=is_offline
)
# 创建并启动解压线程
self.main_window.extraction_thread = self.main_window.create_extraction_thread(

View File

@@ -13,6 +13,7 @@ class GameDetector:
"""
self.game_info = game_info
self.debug_manager = debug_manager
self.directory_cache = {} # 添加目录缓存
def _is_debug_mode(self):
"""检查是否处于调试模式
@@ -135,6 +136,12 @@ class GameDetector:
"""
debug_mode = self._is_debug_mode()
# 检查缓存中是否已有该目录的识别结果
if selected_folder in self.directory_cache:
if debug_mode:
print(f"DEBUG: 使用缓存的目录识别结果: {selected_folder}")
return self.directory_cache[selected_folder]
if debug_mode:
print(f"--- 开始识别目录: {selected_folder} ---")
@@ -307,5 +314,14 @@ class GameDetector:
if debug_mode:
print(f"DEBUG: 最终识别的游戏目录: {game_paths}")
print(f"--- 目录识别结束 ---")
# 将识别结果存入缓存
self.directory_cache[selected_folder] = game_paths
return game_paths
return game_paths
def clear_directory_cache(self):
"""清除目录缓存"""
self.directory_cache = {}
if self._is_debug_mode():
print("DEBUG: 已清除目录缓存")

View File

@@ -291,28 +291,6 @@ class IPv6Manager:
"""
print(f"Toggle IPv6 support: {enabled}")
# 如果用户尝试启用IPv6检查系统是否支持IPv6并发出警告
if enabled:
# 先显示警告提示
warning_msg_box = self._create_message_box(
"警告",
"\n目前IPv6支持功能仍在测试阶段可能会发生意料之外的bug\n\n您确定需要启用吗?\n",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
response = warning_msg_box.exec()
# 如果用户选择不启用,直接返回
if response != QMessageBox.StandardButton.Yes:
return False
# 用户确认启用后继续检查IPv6可用性
ipv6_available = self.check_ipv6_availability()
if not ipv6_available:
msg_box = self._create_message_box("错误", "\n未检测到可用的IPv6连接无法启用IPv6支持。\n\n请确保您的网络环境支持IPv6且已正确配置。\n")
msg_box.exec()
return False
# 保存设置到配置
if self.config is not None:
self.config["ipv6_enabled"] = enabled

View File

@@ -0,0 +1,692 @@
import os
import hashlib
import shutil
import tempfile
import py7zr
from PySide6.QtWidgets import QMessageBox
from data.config import PLUGIN, PLUGIN_HASH, GAME_INFO
from utils import msgbox_frame
class OfflineModeManager:
"""离线模式管理器,用于管理离线模式下的补丁安装和检测"""
def __init__(self, main_window):
"""初始化离线模式管理器
Args:
main_window: 主窗口实例用于访问UI和状态
"""
self.main_window = main_window
self.app_name = main_window.APP_NAME if hasattr(main_window, 'APP_NAME') else ""
self.offline_patches = {} # 存储离线补丁信息 {补丁名称: 文件路径}
self.is_offline_mode = False
def _is_debug_mode(self):
"""检查是否处于调试模式
Returns:
bool: 是否处于调试模式
"""
try:
if hasattr(self.main_window, 'debug_manager') and self.main_window.debug_manager:
if hasattr(self.main_window.debug_manager, '_is_debug_mode'):
# 尝试直接从debug_manager获取状态
return self.main_window.debug_manager._is_debug_mode()
elif hasattr(self.main_window, 'config'):
# 如果debug_manager还没准备好尝试从配置中获取
return self.main_window.config.get('debug_mode', False)
# 如果以上都不可行返回False
return False
except Exception:
# 捕获任何异常默认返回False
return False
def scan_for_offline_patches(self, directory=None):
"""扫描指定目录(默认为软件所在目录)查找离线补丁文件
Args:
directory: 要扫描的目录如果为None则使用软件所在目录
Returns:
dict: 找到的补丁文件 {补丁名称: 文件路径}
"""
if directory is None:
# 获取软件所在目录
directory = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
debug_mode = self._is_debug_mode()
if debug_mode:
print(f"DEBUG: 扫描离线补丁文件,目录: {directory}")
# 要查找的补丁文件名
patch_files = ["vol.1.7z", "vol.2.7z", "vol.3.7z", "vol.4.7z", "after.7z"]
found_patches = {}
# 扫描目录中的文件
for file in os.listdir(directory):
if file.lower() in patch_files:
file_path = os.path.join(directory, file)
if os.path.isfile(file_path):
patch_name = file.lower()
found_patches[patch_name] = file_path
if debug_mode:
print(f"DEBUG: 找到离线补丁文件: {patch_name} 路径: {file_path}")
self.offline_patches = found_patches
return found_patches
def has_offline_patches(self):
"""检查是否有可用的离线补丁文件
Returns:
bool: 是否有可用的离线补丁
"""
if not self.offline_patches:
self.scan_for_offline_patches()
return len(self.offline_patches) > 0
def set_offline_mode(self, enabled):
"""设置离线模式状态
Args:
enabled: 是否启用离线模式
Returns:
bool: 是否成功设置离线模式
"""
debug_mode = self._is_debug_mode()
if enabled:
# 检查是否有离线补丁文件
if not self.has_offline_patches() and not debug_mode:
msgbox_frame(
f"离线模式错误 - {self.app_name}",
"\n未找到任何离线补丁文件,无法启用离线模式。\n\n请将补丁文件放置在软件所在目录后再尝试。\n",
QMessageBox.StandardButton.Ok
).exec()
return False
if debug_mode:
print("DEBUG: 已启用离线模式(调试模式下允许强制启用)")
self.is_offline_mode = enabled
# 更新窗口标题
if hasattr(self.main_window, 'setWindowTitle'):
from data.config import APP_NAME, APP_VERSION
mode_indicator = "[离线模式]" if enabled else "[在线模式]"
self.main_window.setWindowTitle(f"{APP_NAME} v{APP_VERSION} {mode_indicator}")
# 同时更新UI中的标题标签
if hasattr(self.main_window, 'ui') and hasattr(self.main_window.ui, 'title_label'):
self.main_window.ui.title_label.setText(f"{APP_NAME} v{APP_VERSION} {mode_indicator}")
if debug_mode:
print(f"DEBUG: 离线模式已{'启用' if enabled else '禁用'}")
return True
def get_offline_patch_path(self, game_version):
"""根据游戏版本获取对应的离线补丁文件路径
Args:
game_version: 游戏版本名称,如"NEKOPARA Vol.1"
Returns:
str: 离线补丁文件路径如果没有找到则返回None
"""
# 确保已扫描过补丁文件
if not self.offline_patches:
self.scan_for_offline_patches()
# 根据游戏版本获取对应的补丁文件名
patch_file = None
if "Vol.1" in game_version:
patch_file = "vol.1.7z"
elif "Vol.2" in game_version:
patch_file = "vol.2.7z"
elif "Vol.3" in game_version:
patch_file = "vol.3.7z"
elif "Vol.4" in game_version:
patch_file = "vol.4.7z"
elif "After" in game_version:
patch_file = "after.7z"
# 检查是否有对应的补丁文件
if patch_file and patch_file in self.offline_patches:
return self.offline_patches[patch_file]
return None
def prepare_offline_patch(self, game_version, target_path):
"""准备离线补丁文件,复制到缓存目录
Args:
game_version: 游戏版本名称
target_path: 目标路径(通常是缓存目录中的路径)
Returns:
bool: 是否成功准备补丁文件
"""
source_path = self.get_offline_patch_path(game_version)
if not source_path:
return False
debug_mode = self._is_debug_mode()
try:
# 确保目标目录存在
os.makedirs(os.path.dirname(target_path), exist_ok=True)
# 复制文件
shutil.copy2(source_path, target_path)
if debug_mode:
print(f"DEBUG: 已复制离线补丁文件 {source_path}{target_path}")
return True
except Exception as e:
if debug_mode:
print(f"DEBUG: 复制离线补丁文件失败: {e}")
return False
def verify_patch_hash(self, game_version, file_path):
"""验证补丁文件的哈希值
Args:
game_version: 游戏版本名称
file_path: 补丁压缩包文件路径
Returns:
bool: 哈希值是否匹配
"""
# 获取预期的哈希值
expected_hash = None
if "Vol.1" in game_version:
expected_hash = PLUGIN_HASH.get("vol1", "")
elif "Vol.2" in game_version:
expected_hash = PLUGIN_HASH.get("vol2", "")
elif "Vol.3" in game_version:
expected_hash = PLUGIN_HASH.get("vol3", "")
elif "Vol.4" in game_version:
expected_hash = PLUGIN_HASH.get("vol4", "")
elif "After" in game_version:
expected_hash = PLUGIN_HASH.get("after", "")
if not expected_hash:
print(f"DEBUG: 未找到 {game_version} 的预期哈希值")
return False
debug_mode = self._is_debug_mode()
if debug_mode:
print(f"DEBUG: 开始验证离线补丁文件: {file_path}")
print(f"DEBUG: 游戏版本: {game_version}")
print(f"DEBUG: 预期哈希值: {expected_hash}")
try:
# 检查文件是否存在
if not os.path.exists(file_path):
if debug_mode:
print(f"DEBUG: 补丁文件不存在: {file_path}")
return False
# 检查文件大小
file_size = os.path.getsize(file_path)
if debug_mode:
print(f"DEBUG: 补丁文件大小: {file_size} 字节")
if file_size == 0:
if debug_mode:
print(f"DEBUG: 补丁文件大小为0无效文件")
return False
# 创建临时目录用于解压文件
with tempfile.TemporaryDirectory() as temp_dir:
if debug_mode:
print(f"DEBUG: 创建临时目录: {temp_dir}")
# 解压补丁文件
try:
if debug_mode:
print(f"DEBUG: 开始解压文件: {file_path}")
with py7zr.SevenZipFile(file_path, mode="r") as archive:
# 获取压缩包内文件列表
file_list = archive.getnames()
if debug_mode:
print(f"DEBUG: 压缩包内文件列表: {file_list}")
# 解压所有文件
archive.extractall(path=temp_dir)
if debug_mode:
print(f"DEBUG: 解压完成")
# 列出解压后的文件
extracted_files = []
for root, dirs, files in os.walk(temp_dir):
for file in files:
extracted_files.append(os.path.join(root, file))
print(f"DEBUG: 解压后的文件列表: {extracted_files}")
except Exception as e:
if debug_mode:
print(f"DEBUG: 解压补丁文件失败: {e}")
print(f"DEBUG: 错误类型: {type(e).__name__}")
import traceback
print(f"DEBUG: 错误堆栈: {traceback.format_exc()}")
return False
# 获取补丁文件路径
patch_file = None
if "Vol.1" in game_version:
patch_file = os.path.join(temp_dir, "vol.1", "adultsonly.xp3")
elif "Vol.2" in game_version:
patch_file = os.path.join(temp_dir, "vol.2", "adultsonly.xp3")
elif "Vol.3" in game_version:
patch_file = os.path.join(temp_dir, "vol.3", "update00.int")
elif "Vol.4" in game_version:
patch_file = os.path.join(temp_dir, "vol.4", "vol4adult.xp3")
elif "After" in game_version:
patch_file = os.path.join(temp_dir, "after", "afteradult.xp3")
if not patch_file or not os.path.exists(patch_file):
if debug_mode:
print(f"DEBUG: 未找到解压后的补丁文件: {patch_file}")
# 尝试查找可能的替代文件
alternative_files = []
for root, dirs, files in os.walk(temp_dir):
for file in files:
if file.endswith('.xp3') or file.endswith('.int'):
alternative_files.append(os.path.join(root, file))
if alternative_files:
print(f"DEBUG: 找到可能的替代文件: {alternative_files}")
# 检查解压目录结构
print(f"DEBUG: 检查解压目录结构:")
for root, dirs, files in os.walk(temp_dir):
print(f"DEBUG: 目录: {root}")
print(f"DEBUG: 子目录: {dirs}")
print(f"DEBUG: 文件: {files}")
return False
if debug_mode:
print(f"DEBUG: 找到解压后的补丁文件: {patch_file}")
# 计算补丁文件哈希值
try:
with open(patch_file, "rb") as f:
file_hash = hashlib.sha256(f.read()).hexdigest()
# 比较哈希值
result = file_hash.lower() == expected_hash.lower()
if debug_mode:
print(f"DEBUG: 补丁文件 {patch_file} 哈希值验证: {'成功' if result else '失败'}")
print(f"DEBUG: 预期哈希值: {expected_hash}")
print(f"DEBUG: 实际哈希值: {file_hash}")
return result
except Exception as e:
if debug_mode:
print(f"DEBUG: 计算补丁文件哈希值失败: {e}")
print(f"DEBUG: 错误类型: {type(e).__name__}")
return False
except Exception as e:
if debug_mode:
print(f"DEBUG: 验证补丁哈希值失败: {e}")
print(f"DEBUG: 错误类型: {type(e).__name__}")
import traceback
print(f"DEBUG: 错误堆栈: {traceback.format_exc()}")
return False
def is_offline_mode_available(self):
"""检查是否可以使用离线模式
Returns:
bool: 是否可以使用离线模式
"""
# 在调试模式下始终允许离线模式
if self._is_debug_mode():
return True
# 检查是否有离线补丁文件
return self.has_offline_patches()
def is_in_offline_mode(self):
"""检查当前是否处于离线模式
Returns:
bool: 是否处于离线模式
"""
return self.is_offline_mode
def install_offline_patches(self, selected_games):
"""直接安装离线补丁,完全绕过下载模块
Args:
selected_games: 用户选择安装的游戏列表
Returns:
bool: 是否成功启动安装流程
"""
debug_mode = self._is_debug_mode()
if debug_mode:
print(f"DEBUG: 开始离线安装流程,选择的游戏: {selected_games}")
if not self.is_in_offline_mode():
if debug_mode:
print("DEBUG: 当前不是离线模式,无法使用离线安装")
return False
# 确保已扫描过补丁文件
if not self.offline_patches:
self.scan_for_offline_patches()
if not self.offline_patches:
if debug_mode:
print("DEBUG: 未找到任何离线补丁文件")
msgbox_frame(
f"离线安装错误 - {self.app_name}",
"\n未找到任何离线补丁文件,无法进行离线安装。\n\n请将补丁文件放置在软件所在目录后再尝试。\n",
QMessageBox.StandardButton.Ok
).exec()
return False
# 获取游戏目录
game_dirs = self.main_window.game_detector.identify_game_directories_improved(
self.main_window.download_manager.selected_folder
)
if not game_dirs:
if debug_mode:
print("DEBUG: 未识别到任何游戏目录")
return False
# 显示文件检验窗口
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="pre", is_offline=True)
# 获取安装路径
install_paths = self.main_window.download_manager.get_install_paths()
# 创建并启动哈希线程进行预检查
self.main_window.hash_thread = self.main_window.create_hash_thread("pre", install_paths)
self.main_window.hash_thread.pre_finished.connect(
lambda updated_status: self.on_offline_pre_hash_finished(updated_status, game_dirs, selected_games)
)
self.main_window.hash_thread.start()
return True
def on_offline_pre_hash_finished(self, updated_status, game_dirs, selected_games):
"""离线模式下的哈希预检查完成处理
Args:
updated_status: 更新后的安装状态
game_dirs: 识别到的游戏目录
selected_games: 用户选择安装的游戏列表
"""
debug_mode = self._is_debug_mode()
# 更新安装状态
self.main_window.installed_status = updated_status
# 关闭哈希检查窗口
if self.main_window.hash_msg_box and self.main_window.hash_msg_box.isVisible():
self.main_window.hash_msg_box.accept()
self.main_window.hash_msg_box = None
# 重新启用主窗口
self.main_window.setEnabled(True)
# 过滤出需要安装的游戏
installable_games = []
for game_version in selected_games:
if game_version in game_dirs and not self.main_window.installed_status.get(game_version, False):
# 检查是否有对应的离线补丁
if self.get_offline_patch_path(game_version):
installable_games.append(game_version)
elif debug_mode:
print(f"DEBUG: 未找到 {game_version} 的离线补丁文件,跳过")
if not installable_games:
if debug_mode:
print("DEBUG: 没有需要安装的游戏或未找到对应的离线补丁")
msgbox_frame(
f"离线安装信息 - {self.app_name}",
"\n没有需要安装的游戏或未找到对应的离线补丁文件。\n",
QMessageBox.StandardButton.Ok
).exec()
self.main_window.ui.start_install_text.setText("开始安装")
return
# 开始安装流程
if debug_mode:
print(f"DEBUG: 开始离线安装流程,安装游戏: {installable_games}")
# 创建安装任务列表
install_tasks = []
for game_version in installable_games:
# 获取离线补丁文件路径
patch_file = self.get_offline_patch_path(game_version)
if not patch_file:
continue
# 获取游戏目录
game_folder = game_dirs.get(game_version)
if not game_folder:
continue
# 获取目标路径
if "Vol.1" in game_version:
_7z_path = os.path.join(PLUGIN, "vol.1.7z")
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
elif "Vol.2" in game_version:
_7z_path = os.path.join(PLUGIN, "vol.2.7z")
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
elif "Vol.3" in game_version:
_7z_path = os.path.join(PLUGIN, "vol.3.7z")
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
elif "Vol.4" in game_version:
_7z_path = os.path.join(PLUGIN, "vol.4.7z")
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
elif "After" in game_version:
_7z_path = os.path.join(PLUGIN, "after.7z")
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
else:
continue
# 添加到安装任务列表
install_tasks.append((patch_file, game_folder, game_version, _7z_path, plugin_path))
# 开始执行第一个安装任务
if install_tasks:
self.process_next_offline_install_task(install_tasks)
else:
self.main_window.ui.start_install_text.setText("开始安装")
def process_next_offline_install_task(self, install_tasks):
"""处理下一个离线安装任务
Args:
install_tasks: 安装任务列表,每个任务是一个元组 (patch_file, game_folder, game_version, _7z_path, plugin_path)
"""
debug_mode = self._is_debug_mode()
if not install_tasks:
# 所有任务完成,进行后检查
if debug_mode:
print("DEBUG: 所有离线安装任务完成,进行后检查")
self.main_window.after_hash_compare()
return
# 获取下一个任务
patch_file, game_folder, game_version, _7z_path, plugin_path = install_tasks.pop(0)
if debug_mode:
print(f"DEBUG: 处理离线安装任务: {game_version}")
print(f"DEBUG: 补丁文件: {patch_file}")
print(f"DEBUG: 游戏目录: {game_folder}")
# 确保目标目录存在
os.makedirs(os.path.dirname(_7z_path), exist_ok=True)
try:
# 复制补丁文件到缓存目录
shutil.copy2(patch_file, _7z_path)
if debug_mode:
print(f"DEBUG: 已复制补丁文件到缓存目录: {_7z_path}")
print(f"DEBUG: 开始验证补丁文件哈希值")
# 获取预期的哈希值
expected_hash = None
if "Vol.1" in game_version:
expected_hash = PLUGIN_HASH.get("vol1", "")
elif "Vol.2" in game_version:
expected_hash = PLUGIN_HASH.get("vol2", "")
elif "Vol.3" in game_version:
expected_hash = PLUGIN_HASH.get("vol3", "")
elif "Vol.4" in game_version:
expected_hash = PLUGIN_HASH.get("vol4", "")
elif "After" in game_version:
expected_hash = PLUGIN_HASH.get("after", "")
if debug_mode and expected_hash:
print(f"DEBUG: 预期哈希值: {expected_hash}")
# 显示哈希验证窗口 - 使用离线特定消息
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="offline_verify", is_offline=True)
# 验证补丁文件哈希
hash_valid = self.verify_patch_hash(game_version, _7z_path)
# 关闭哈希验证窗口
if self.main_window.hash_msg_box and self.main_window.hash_msg_box.isVisible():
self.main_window.hash_msg_box.close()
self.main_window.hash_msg_box = None
if hash_valid:
if debug_mode:
print(f"DEBUG: 补丁文件哈希验证成功,开始解压")
# 显示解压窗口 - 使用离线特定消息
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="offline_extraction", is_offline=True)
try:
# 创建解压线程
extraction_thread = self.main_window.create_extraction_thread(
_7z_path, game_folder, plugin_path, game_version
)
# 正确连接信号
extraction_thread.finished.connect(
lambda success, error, game_ver: self.on_extraction_thread_finished(
success, error, game_ver, install_tasks
)
)
# 启动解压线程
extraction_thread.start()
except Exception as e:
if debug_mode:
print(f"DEBUG: 创建或启动解压线程失败: {e}")
# 关闭解压窗口
if self.main_window.hash_msg_box and self.main_window.hash_msg_box.isVisible():
self.main_window.hash_msg_box.close()
self.main_window.hash_msg_box = None
# 显示错误消息
msgbox_frame(
f"解压错误 - {self.app_name}",
f"\n{game_version} 的解压过程中发生错误: {str(e)}\n\n跳过此游戏的安装。\n",
QMessageBox.StandardButton.Ok
).exec()
# 继续下一个任务
self.process_next_offline_install_task(install_tasks)
else:
if debug_mode:
print(f"DEBUG: 补丁文件哈希验证失败")
# 显示错误消息
msgbox_frame(
f"哈希验证失败 - {self.app_name}",
f"\n{game_version} 的补丁文件哈希验证失败,可能已损坏或被篡改。\n\n跳过此游戏的安装。\n",
QMessageBox.StandardButton.Ok
).exec()
# 继续下一个任务
self.process_next_offline_install_task(install_tasks)
except Exception as e:
if debug_mode:
print(f"DEBUG: 离线安装任务处理失败: {e}")
# 显示错误消息
msgbox_frame(
f"安装错误 - {self.app_name}",
f"\n{game_version} 的安装过程中发生错误: {str(e)}\n\n跳过此游戏的安装。\n",
QMessageBox.StandardButton.Ok
).exec()
# 继续下一个任务
self.process_next_offline_install_task(install_tasks)
def on_extraction_thread_finished(self, success, error_message, game_version, remaining_tasks):
"""解压线程完成后的处理
Args:
success: 是否解压成功
error_message: 错误信息
game_version: 游戏版本
remaining_tasks: 剩余的安装任务列表
"""
debug_mode = self._is_debug_mode()
# 关闭解压窗口
if self.main_window.hash_msg_box and self.main_window.hash_msg_box.isVisible():
self.main_window.hash_msg_box.close()
self.main_window.hash_msg_box = None
if debug_mode:
print(f"DEBUG: 离线解压完成,状态: {'成功' if success else '失败'}")
if not success:
print(f"DEBUG: 错误信息: {error_message}")
if not success:
# 显示错误消息
msgbox_frame(
f"解压失败 - {self.app_name}",
f"\n{game_version} 的补丁解压失败。\n\n错误信息: {error_message}\n\n跳过此游戏的安装。\n",
QMessageBox.StandardButton.Ok
).exec()
# 更新安装状态
self.main_window.installed_status[game_version] = False
else:
# 更新安装状态
self.main_window.installed_status[game_version] = True
# 处理下一个任务
self.process_next_offline_install_task(remaining_tasks)
def on_offline_extraction_finished(self, remaining_tasks):
"""离线模式下的解压完成处理(旧方法,保留兼容性)
Args:
remaining_tasks: 剩余的安装任务列表
"""
debug_mode = self._is_debug_mode()
if debug_mode:
print("DEBUG: 离线解压完成,继续处理下一个任务")
# 处理下一个任务
self.process_next_offline_install_task(remaining_tasks)

View File

@@ -1,4 +1,4 @@
from PySide6.QtGui import QIcon, QAction, QFont, QCursor
from PySide6.QtGui import QIcon, QAction, QFont, QCursor, QActionGroup
from PySide6.QtWidgets import QMessageBox, QMainWindow, QMenu, QPushButton
from PySide6.QtCore import Qt, QRect
import webbrowser
@@ -37,8 +37,18 @@ class UIManager:
if os.path.exists(icon_path):
self.main_window.setWindowIcon(QIcon(icon_path))
# 设置窗口标题
self.main_window.setWindowTitle(f"{APP_NAME} v{APP_VERSION}")
# 获取当前离线模式状态
is_offline_mode = False
if hasattr(self.main_window, 'offline_mode_manager'):
is_offline_mode = self.main_window.offline_mode_manager.is_in_offline_mode()
# 设置窗口标题和UI标题标签
mode_indicator = "[离线模式]" if is_offline_mode else "[在线模式]"
self.main_window.setWindowTitle(f"{APP_NAME} v{APP_VERSION} {mode_indicator}")
# 更新UI中的标题标签
if hasattr(self.main_window, 'ui') and hasattr(self.main_window.ui, 'title_label'):
self.main_window.ui.title_label.setText(f"{APP_NAME} v{APP_VERSION} {mode_indicator}")
# 创建关于按钮
self._create_about_button()
@@ -265,6 +275,34 @@ class UIManager:
menu_font = self._get_menu_font()
font_family = menu_font.family()
# 创建工作模式子菜单
self.work_mode_menu = QMenu("工作模式", self.main_window)
self.work_mode_menu.setFont(menu_font)
self.work_mode_menu.setStyleSheet(self._get_menu_style(font_family))
# 创建在线模式和离线模式选项
self.online_mode_action = QAction("在线模式", self.main_window, checkable=True)
self.online_mode_action.setFont(menu_font)
self.online_mode_action.setChecked(True) # 默认选中在线模式
self.offline_mode_action = QAction("离线模式", self.main_window, checkable=True)
self.offline_mode_action.setFont(menu_font)
self.offline_mode_action.setChecked(False)
# 将两个模式选项添加到同一个互斥组
mode_group = QActionGroup(self.main_window)
mode_group.addAction(self.online_mode_action)
mode_group.addAction(self.offline_mode_action)
mode_group.setExclusive(True) # 确保只能选择一个模式
# 连接切换事件
self.online_mode_action.triggered.connect(lambda: self.switch_work_mode("online"))
self.offline_mode_action.triggered.connect(lambda: self.switch_work_mode("offline"))
# 添加到工作模式子菜单
self.work_mode_menu.addAction(self.online_mode_action)
self.work_mode_menu.addAction(self.offline_mode_action)
# 创建开发者选项子菜单
self.dev_menu = QMenu("开发者选项", self.main_window)
self.dev_menu.setFont(menu_font) # 设置与UI_install.py中相同的字体
@@ -300,28 +338,11 @@ class UIManager:
self.ipv6_submenu.setFont(menu_font)
self.ipv6_submenu.setStyleSheet(menu_style)
# 检查IPv6是否可用
ipv6_available = False
if self.ipv6_manager:
ipv6_available = self.ipv6_manager.check_ipv6_availability()
if not ipv6_available:
self.ipv6_action.setText("启用IPv6支持 (不可用)")
self.ipv6_action.setEnabled(False)
self.ipv6_action.setToolTip("未检测到可用的IPv6连接")
# 检查配置中是否已启用IPv6
config = getattr(self.main_window, 'config', {})
ipv6_enabled = False
if isinstance(config, dict):
ipv6_enabled = config.get("ipv6_enabled", False)
# 如果配置中启用了IPv6但实际不可用则强制禁用
if ipv6_enabled and not ipv6_available:
config["ipv6_enabled"] = False
ipv6_enabled = False
# 使用utils.save_config直接保存配置
from utils import save_config
save_config(config)
self.ipv6_action.setChecked(ipv6_enabled)
@@ -416,6 +437,7 @@ class UIManager:
self.download_settings_menu.addAction(self.thread_settings_action)
# 添加到主菜单
self.ui.menu.addMenu(self.work_mode_menu) # 添加工作模式子菜单
self.ui.menu.addMenu(self.download_settings_menu) # 添加下载设置子菜单
self.ui.menu.addSeparator()
self.ui.menu.addMenu(self.dev_menu) # 添加开发者选项子菜单
@@ -438,7 +460,47 @@ class UIManager:
# 恢复复选框状态
self.ipv6_action.setChecked(not enabled)
return
if enabled:
# 先显示警告提示
warning_msg_box = self._create_message_box(
"警告",
"\n目前IPv6支持功能仍在测试阶段可能会发生意料之外的bug\n\n您确定需要启用吗?\n",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
response = warning_msg_box.exec()
# 如果用户选择不启用,直接返回
if response != QMessageBox.StandardButton.Yes:
# 恢复复选框状态
self.ipv6_action.setChecked(False)
return
# 显示正在校验IPv6的提示
msg_box = self._create_message_box("IPv6检测", "\n正在校验是否支持IPv6请稍候...\n")
msg_box.open() # 使用open而不是exec这样不会阻塞UI
# 处理消息队列,确保对话框显示
from PySide6.QtCore import QCoreApplication
QCoreApplication.processEvents()
# 检查IPv6是否可用
ipv6_available = self.ipv6_manager.check_ipv6_availability()
# 关闭提示对话框
msg_box.accept()
if not ipv6_available:
# 显示IPv6不可用的提示
error_msg_box = self._create_message_box(
"IPv6不可用",
"\n未检测到可用的IPv6连接无法启用IPv6支持。\n\n请确保您的网络环境支持IPv6且已正确配置。\n"
)
error_msg_box.exec()
# 恢复复选框状态
self.ipv6_action.setChecked(False)
return False
# 使用IPv6Manager处理切换
success = self.ipv6_manager.toggle_ipv6_support(enabled)
# 如果切换失败,恢复复选框状态
@@ -732,4 +794,72 @@ class UIManager:
def show_ipv6_manager_not_ready(self):
"""显示IPv6管理器未准备好的提示"""
msg_box = self._create_message_box("错误", "\nIPv6管理器尚未初始化请稍后再试。\n")
msg_box.exec()
msg_box.exec()
def switch_work_mode(self, mode):
"""切换工作模式
Args:
mode: 要切换的模式,"online""offline"
"""
# 检查主窗口是否有离线模式管理器
if not hasattr(self.main_window, 'offline_mode_manager'):
# 如果没有离线模式管理器,创建提示
msg_box = self._create_message_box(
"错误",
"\n离线模式管理器未初始化,无法切换工作模式。\n"
)
msg_box.exec()
# 恢复选择状态
self.online_mode_action.setChecked(True)
self.offline_mode_action.setChecked(False)
return
if mode == "offline":
# 尝试切换到离线模式
success = self.main_window.offline_mode_manager.set_offline_mode(True)
if not success:
# 如果切换失败,恢复选择状态
self.online_mode_action.setChecked(True)
self.offline_mode_action.setChecked(False)
return
# 更新配置
self.main_window.config["offline_mode"] = True
self.main_window.save_config(self.main_window.config)
# 在离线模式下始终启用开始安装按钮
if hasattr(self.main_window, 'set_start_button_enabled'):
self.main_window.set_start_button_enabled(True)
# 清除版本警告标志
if hasattr(self.main_window, 'version_warning'):
self.main_window.version_warning = False
# 显示提示
msg_box = self._create_message_box(
"模式已切换",
"\n已切换到离线模式。\n\n将使用本地补丁文件进行安装,不会从网络下载补丁。\n"
)
msg_box.exec()
else:
# 切换到在线模式
self.main_window.offline_mode_manager.set_offline_mode(False)
# 更新配置
self.main_window.config["offline_mode"] = False
self.main_window.save_config(self.main_window.config)
# 如果当前版本过低,设置版本警告标志
if hasattr(self.main_window, 'last_error_message') and self.main_window.last_error_message == "update_required":
# 设置版本警告标志
if hasattr(self.main_window, 'version_warning'):
self.main_window.version_warning = True
# 显示提示
msg_box = self._create_message_box(
"模式已切换",
"\n已切换到在线模式。\n\n将从网络下载补丁进行安装。\n"
)
msg_box.exec()

View File

@@ -3,7 +3,7 @@ import base64
# 配置信息
app_data = {
"APP_VERSION": "1.3.2",
"APP_VERSION": "1.4.0",
"APP_NAME": "FRAISEMOE Addons Installer NEXT",
"TEMP": "TEMP",
"CACHE": "FRAISEMOE",
@@ -62,7 +62,13 @@ UA = app_data["UA_TEMPLATE"].format(APP_VERSION)
GAME_INFO = app_data["game_info"]
BLOCK_SIZE = 67108864
HASH_SIZE = 134217728
PLUGIN_HASH = {game: info["hash"] for game, info in GAME_INFO.items()}
PLUGIN_HASH = {
"vol1": GAME_INFO["NEKOPARA Vol.1"]["hash"],
"vol2": GAME_INFO["NEKOPARA Vol.2"]["hash"],
"vol3": GAME_INFO["NEKOPARA Vol.3"]["hash"],
"vol4": GAME_INFO["NEKOPARA Vol.4"]["hash"],
"after": GAME_INFO["NEKOPARA After"]["hash"]
}
PROCESS_INFO = {info["exe"]: game for game, info in GAME_INFO.items()}
# 下载线程档位设置

View File

@@ -15,7 +15,7 @@ from ui.Ui_install import Ui_MainWindows
from data.config import (
APP_NAME, PLUGIN, GAME_INFO, BLOCK_SIZE,
PLUGIN_HASH, UA, CONFIG_URL, LOG_FILE,
DOWNLOAD_THREADS, DEFAULT_DOWNLOAD_THREAD_LEVEL # 添加下载线程常量
DOWNLOAD_THREADS, DEFAULT_DOWNLOAD_THREAD_LEVEL, APP_VERSION # 添加APP_VERSION导入
)
from utils import (
load_config, save_config, HashManager, AdminPrivileges, msgbox_frame, load_image_from_file
@@ -57,7 +57,7 @@ class MainWindow(QMainWindow):
self.hash_manager = HashManager(BLOCK_SIZE)
self.admin_privileges = AdminPrivileges()
# 初始化各种管理器
# 初始化各种管理器 - 调整初始化顺序,避免循环依赖
# 1. 首先创建必要的基础管理器
self.animator = MultiStageAnimations(self.ui, self)
self.window_manager = WindowManager(self)
@@ -72,18 +72,19 @@ class MainWindow(QMainWindow):
# 4. 为debug_manager设置ui_manager引用
self.debug_manager.set_ui_manager(self.ui_manager)
# 设置UI - 确保debug_action已初始化
self.ui_manager.setup_ui()
# 5. 初始化其他管理器
self.config_manager = ConfigManager(APP_NAME, CONFIG_URL, UA, self.debug_manager)
self.game_detector = GameDetector(GAME_INFO, self.debug_manager)
self.patch_manager = PatchManager(APP_NAME, GAME_INFO, self.debug_manager)
# 6. 初始化下载管理器 - 放在最后,因为它可能依赖于其他管理器
# 6. 初始化离线模式管理器
from core.offline_mode_manager import OfflineModeManager
self.offline_mode_manager = OfflineModeManager(self)
# 7. 初始化下载管理器 - 放在最后,因为它可能依赖于其他管理器
self.download_manager = DownloadManager(self)
# 7. 初始化功能处理程序
# 8. 初始化功能处理程序
self.uninstall_handler = UninstallHandler(self)
self.patch_toggle_handler = PatchToggleHandler(self)
@@ -97,6 +98,9 @@ class MainWindow(QMainWindow):
self.patch_manager.initialize_status()
self.installed_status = self.patch_manager.get_status() # 获取初始化后的状态
self.hash_msg_box = None
self.last_error_message = "" # 添加错误信息记录
self.version_warning = False # 添加版本警告标志
self.install_button_enabled = True # 默认启用安装按钮
self.progress_window = None
# 设置关闭按钮事件连接
@@ -140,11 +144,17 @@ class MainWindow(QMainWindow):
if self.ui_manager.debug_action.isChecked():
self.debug_manager.start_logging()
# 在窗口显示前设置初始状态
self.animator.initialize()
# 设置UI包括窗口图标和菜单
self.ui_manager.setup_ui()
# 窗口显示后延迟100ms启动动画
QTimer.singleShot(100, self.start_animations)
# 检查是否有离线补丁文件,如果有则自动切换到离线模式
self.check_and_set_offline_mode()
# 获取云端配置
self.fetch_cloud_config()
# 启动动画
self.start_animations()
# 窗口事件处理 - 委托给WindowManager
def mousePressEvent(self, event):
@@ -170,10 +180,14 @@ class MainWindow(QMainWindow):
# 但确保开始安装按钮仍然处于禁用状态
self.set_start_button_enabled(False)
# 在动画开始前初始化
self.animator.initialize()
# 连接动画完成信号
self.animator.animation_finished.connect(self.on_animations_finished)
# 启动动画
self.animator.start_animations()
# 在动画开始时获取云端配置
self.fetch_cloud_config()
def on_animations_finished(self):
"""动画完成后启用按钮"""
@@ -185,8 +199,16 @@ class MainWindow(QMainWindow):
self.ui.toggle_patch_btn.setEnabled(True) # 启用禁/启用补丁按钮
self.ui.exit_btn.setEnabled(True)
# 只有在配置有效时才启用开始安装按钮
if self.config_valid:
# 检查是否处于离线模式
is_offline_mode = False
if hasattr(self, 'offline_mode_manager'):
is_offline_mode = self.offline_mode_manager.is_in_offline_mode()
# 如果是离线模式,始终启用开始安装按钮
if is_offline_mode:
self.set_start_button_enabled(True)
# 否则,只有在配置有效时才启用开始安装按钮
elif self.config_valid:
self.set_start_button_enabled(True)
else:
self.set_start_button_enabled(False)
@@ -237,14 +259,14 @@ class MainWindow(QMainWindow):
elif result["action"] == "disable_button":
# 禁用开始安装按钮
self.set_start_button_enabled(False)
# 检查是否有后续操作
if "then" in result and result["then"] == "exit":
# 强制关闭程序
self.shutdown_app(force_exit=True)
elif result["action"] == "enable_button":
# 启用开始安装按钮
self.set_start_button_enabled(True)
# 检查是否需要记录版本警告
if "version_warning" in result and result["version_warning"]:
self.version_warning = True
else:
self.version_warning = False
# 同步状态
self.cloud_config = self.config_manager.get_cloud_config()
@@ -318,7 +340,12 @@ class MainWindow(QMainWindow):
"""进行安装后哈希比较"""
# 禁用窗口已在安装流程开始时完成
self.hash_msg_box = self.hash_manager.hash_pop_window(check_type="after")
# 检查是否处于离线模式
is_offline = False
if hasattr(self, 'offline_mode_manager'):
is_offline = self.offline_mode_manager.is_in_offline_mode()
self.hash_msg_box = self.hash_manager.hash_pop_window(check_type="after", is_offline=is_offline)
install_paths = self.download_manager.get_install_paths()
@@ -495,16 +522,28 @@ class MainWindow(QMainWindow):
"""处理安装按钮点击事件
根据按钮当前状态决定是显示错误还是执行安装
"""
# 检查是否处于离线模式
is_offline_mode = False
if hasattr(self, 'offline_mode_manager'):
is_offline_mode = self.offline_mode_manager.is_in_offline_mode()
# 如果版本过低且在在线模式下,提示用户更新
if self.last_error_message == "update_required" and not is_offline_mode:
# 在线模式下提示用户更新软件
msg_box = msgbox_frame(
f"更新提示 - {APP_NAME}",
"\n当前版本过低,请及时更新。\n如需联网下载补丁,请更新到最新版,否则无法下载。\n\n是否切换到离线模式继续使用?\n",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if msg_box.exec() == QMessageBox.StandardButton.Yes:
# 切换到离线模式
if self.ui_manager and hasattr(self.ui_manager, 'switch_work_mode'):
self.ui_manager.switch_work_mode("offline")
return
if not self.install_button_enabled:
# 按钮处于"无法安装"状态
if self.last_error_message == "update_required":
msg_box = msgbox_frame(
f"更新提示 - {APP_NAME}",
"\n当前版本过低,请及时更新。\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
elif self.last_error_message == "directory_not_found":
if self.last_error_message == "directory_not_found":
# 目录识别失败的特定错误信息
reply = msgbox_frame(
f"目录错误 - {APP_NAME}",
@@ -517,18 +556,299 @@ class MainWindow(QMainWindow):
# 直接调用文件对话框
self.download_manager.file_dialog()
else:
# 网络错误或其他错误
reply = msgbox_frame(
f"错误 - {APP_NAME}",
"\n访问云端配置失败,请检查网络状况或稍后再试。\n\n是否重新尝试连接?\n",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply.exec() == QMessageBox.StandardButton.Yes:
# 重试获取配置
self.fetch_cloud_config()
# 检查是否处于离线模式
if is_offline_mode and self.last_error_message == "network_error":
# 如果是离线模式且错误是网络相关的,提示切换到在线模式
reply = msgbox_frame(
f"离线模式提示 - {APP_NAME}",
"\n当前处于离线模式,但本地补丁文件不完整。\n\n是否切换到在线模式尝试下载?\n",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply.exec() == QMessageBox.StandardButton.Yes:
# 切换到在线模式
if self.ui_manager and hasattr(self.ui_manager, 'switch_work_mode'):
self.ui_manager.switch_work_mode("online")
# 重试获取配置
self.fetch_cloud_config()
else:
# 网络错误或其他错误
reply = msgbox_frame(
f"错误 - {APP_NAME}",
"\n访问云端配置失败,请检查网络状况或稍后再试。\n\n是否重新尝试连接?\n",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply.exec() == QMessageBox.StandardButton.Yes:
# 重试获取配置
self.fetch_cloud_config()
else:
# 按钮处于"开始安装"状态,正常执行安装流程
self.download_manager.file_dialog()
# 检查是否处于离线模式
if is_offline_mode:
# 如果是离线模式,使用离线安装流程
# 先选择游戏目录
self.selected_folder = QtWidgets.QFileDialog.getExistingDirectory(
self, f"选择游戏所在【上级目录】 {APP_NAME}"
)
if not self.selected_folder:
QtWidgets.QMessageBox.warning(
self, f"通知 - {APP_NAME}", "\n未选择任何目录,请重新选择\n"
)
return
# 保存选择的目录到下载管理器
self.download_manager.selected_folder = self.selected_folder
# 设置按钮状态
self.ui.start_install_text.setText("正在安装")
self.setEnabled(False)
# 清除游戏检测器的目录缓存
if hasattr(self, 'game_detector') and hasattr(self.game_detector, 'clear_directory_cache'):
self.game_detector.clear_directory_cache()
# 识别游戏目录
game_dirs = self.game_detector.identify_game_directories_improved(self.selected_folder)
if not game_dirs:
self.last_error_message = "directory_not_found"
QtWidgets.QMessageBox.warning(
self,
f"目录错误 - {APP_NAME}",
"\n未能识别到任何游戏目录。\n\n请确认您选择的是游戏的上级目录并且该目录中包含NEKOPARA系列游戏文件夹。\n"
)
self.setEnabled(True)
self.ui.start_install_text.setText("开始安装")
return
# 显示游戏选择对话框
dialog = QtWidgets.QDialog(self)
dialog.setWindowTitle("选择要安装的游戏")
dialog.resize(400, 300)
# 设置对话框样式
dialog.setStyleSheet("""
QDialog {
background-color: #2D2D30;
color: #FFFFFF;
}
QCheckBox {
color: #FFFFFF;
font-size: 14px;
padding: 5px;
margin: 5px;
}
QCheckBox:hover {
background-color: #3E3E42;
border-radius: 4px;
}
QCheckBox:checked {
color: #F47A5B;
}
QPushButton {
background-color: #3E3E42;
color: #FFFFFF;
border: none;
border-radius: 4px;
padding: 8px 16px;
font-size: 14px;
min-width: 100px;
}
QPushButton:hover {
background-color: #F47A5B;
}
QPushButton:pressed {
background-color: #E06A4B;
}
""")
layout = QtWidgets.QVBoxLayout(dialog)
layout.setContentsMargins(20, 20, 20, 20)
layout.setSpacing(10)
# 添加标题标签
title_label = QtWidgets.QLabel("选择要安装的游戏", dialog)
title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #F47A5B; margin-bottom: 10px;")
layout.addWidget(title_label)
# 添加分隔线
line = QtWidgets.QFrame(dialog)
line.setFrameShape(QtWidgets.QFrame.Shape.HLine)
line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken)
line.setStyleSheet("background-color: #3E3E42; margin: 10px 0px;")
layout.addWidget(line)
# 添加游戏选择框
game_checkboxes = {}
scroll_area = QtWidgets.QScrollArea(dialog)
scroll_area.setWidgetResizable(True)
scroll_area.setStyleSheet("border: none; background-color: transparent;")
scroll_content = QtWidgets.QWidget(scroll_area)
scroll_layout = QtWidgets.QVBoxLayout(scroll_content)
scroll_layout.setContentsMargins(5, 5, 5, 5)
scroll_layout.setSpacing(8)
scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
for game_version in game_dirs.keys():
checkbox = QtWidgets.QCheckBox(game_version, scroll_content)
checkbox.setChecked(True) # 默认选中
scroll_layout.addWidget(checkbox)
game_checkboxes[game_version] = checkbox
scroll_content.setLayout(scroll_layout)
scroll_area.setWidget(scroll_content)
layout.addWidget(scroll_area)
# 添加按钮
button_layout = QtWidgets.QHBoxLayout()
button_layout.setSpacing(15)
# 全选按钮
select_all_btn = QtWidgets.QPushButton("全选", dialog)
select_all_btn.clicked.connect(lambda: self.select_all_games(game_checkboxes, True))
# 全不选按钮
deselect_all_btn = QtWidgets.QPushButton("全不选", dialog)
deselect_all_btn.clicked.connect(lambda: self.select_all_games(game_checkboxes, False))
# 确定和取消按钮
ok_button = QtWidgets.QPushButton("确定", dialog)
ok_button.setStyleSheet(ok_button.styleSheet() + "background-color: #007ACC;")
cancel_button = QtWidgets.QPushButton("取消", dialog)
# 添加按钮到布局
button_layout.addWidget(select_all_btn)
button_layout.addWidget(deselect_all_btn)
button_layout.addStretch()
button_layout.addWidget(ok_button)
button_layout.addWidget(cancel_button)
layout.addLayout(button_layout)
# 连接信号
ok_button.clicked.connect(dialog.accept)
cancel_button.clicked.connect(dialog.reject)
# 显示对话框
if dialog.exec() == QtWidgets.QDialog.DialogCode.Accepted:
# 获取选择的游戏
selected_games = []
for game_version, checkbox in game_checkboxes.items():
if checkbox.isChecked():
selected_games.append(game_version)
if selected_games:
# 使用离线模式管理器进行安装
self.offline_mode_manager.install_offline_patches(selected_games)
else:
QtWidgets.QMessageBox.information(
self,
f"通知 - {APP_NAME}",
"\n未选择任何游戏,安装已取消。\n"
)
self.setEnabled(True)
self.ui.start_install_text.setText("开始安装")
else:
# 用户取消了选择
self.setEnabled(True)
self.ui.start_install_text.setText("开始安装")
else:
# 在线模式下,检查版本是否过低
if hasattr(self, 'version_warning') and self.version_warning:
# 版本过低,提示用户更新
msg_box = msgbox_frame(
f"版本过低 - {APP_NAME}",
"\n当前版本过低,无法使用在线下载功能。\n\n请更新到最新版本或切换到离线模式。\n",
QMessageBox.StandardButton.Ok
)
msg_box.exec()
else:
# 版本正常,使用原有的下载流程
self.download_manager.file_dialog()
def check_and_set_offline_mode(self):
"""检查是否有离线补丁文件,如果有则自动切换到离线模式"""
try:
# 检查是否有离线补丁文件
has_offline_patches = self.offline_mode_manager.has_offline_patches()
# 获取调试模式状态
debug_mode = False
if hasattr(self.debug_manager, '_is_debug_mode'):
debug_mode = self.debug_manager._is_debug_mode()
# 检查配置中是否已设置离线模式
offline_mode_enabled = False
if isinstance(self.config, dict):
offline_mode_enabled = self.config.get("offline_mode", False)
# 如果有离线补丁文件或者调试模式下强制启用离线模式
if has_offline_patches or (debug_mode and offline_mode_enabled):
# 设置离线模式
self.offline_mode_manager.set_offline_mode(True)
# 更新UI中的离线模式选项
if hasattr(self.ui_manager, 'offline_mode_action') and self.ui_manager.offline_mode_action:
self.ui_manager.offline_mode_action.setChecked(True)
self.ui_manager.online_mode_action.setChecked(False)
# 更新配置
self.config["offline_mode"] = True
self.save_config(self.config)
# 在离线模式下始终启用开始安装按钮
self.set_start_button_enabled(True)
# 清除版本警告标志
self.version_warning = False
if debug_mode:
print(f"DEBUG: 已自动切换到离线模式,找到离线补丁文件: {list(self.offline_mode_manager.offline_patches.keys())}")
print(f"DEBUG: 离线模式下启用开始安装按钮")
else:
# 如果没有离线补丁文件,确保使用在线模式
self.offline_mode_manager.set_offline_mode(False)
# 更新UI中的在线模式选项
if hasattr(self.ui_manager, 'online_mode_action') and self.ui_manager.online_mode_action:
self.ui_manager.online_mode_action.setChecked(True)
self.ui_manager.offline_mode_action.setChecked(False)
# 更新配置
self.config["offline_mode"] = False
self.save_config(self.config)
# 如果当前版本过低,设置版本警告标志
if hasattr(self, 'last_error_message') and self.last_error_message == "update_required":
# 设置版本警告标志
self.version_warning = True
if debug_mode:
print("DEBUG: 未找到离线补丁文件,使用在线模式")
# 确保标题标签显示正确的模式
if hasattr(self, 'ui') and hasattr(self.ui, 'title_label'):
from data.config import APP_NAME, APP_VERSION
mode_indicator = "[离线模式]" if self.offline_mode_manager.is_in_offline_mode() else "[在线模式]"
self.ui.title_label.setText(f"{APP_NAME} v{APP_VERSION} {mode_indicator}")
except Exception as e:
# 捕获任何异常,确保程序不会崩溃
print(f"错误: 检查离线模式时发生异常: {e}")
# 默认使用在线模式
if hasattr(self, 'offline_mode_manager'):
self.offline_mode_manager.is_offline_mode = False
def select_all_games(self, game_checkboxes, checked):
"""选择或取消选择所有游戏
Args:
game_checkboxes: 游戏复选框字典
checked: 是否选中
"""
for checkbox in game_checkboxes.values():
checkbox.setChecked(checked)

View File

@@ -542,7 +542,6 @@ class Ui_MainWindows(object):
# setupUi
def retranslateUi(self, MainWindows):
MainWindows.setWindowTitle(QCoreApplication.translate("MainWindows", f"{APP_NAME} v{APP_VERSION}", None))
self.loadbg.setText("")
self.vol1bg.setText("")
self.vol2bg.setText("")

View File

@@ -112,23 +112,38 @@ class HashManager:
print(f"Error calculating hash for {file_path}: {e}")
return results
def hash_pop_window(self, check_type="default"):
def hash_pop_window(self, check_type="default", is_offline=False):
"""显示文件检验窗口
Args:
check_type: 检查类型,可以是 'pre'(预检查), 'after'(后检查), 'extraction'(解压后检查)
check_type: 检查类型,可以是 'pre'(预检查), 'after'(后检查), 'extraction'(解压后检查), 'offline_extraction'(离线解压), 'offline_verify'(离线验证)
is_offline: 是否处于离线模式
Returns:
QMessageBox: 消息框实例
"""
message = "\n正在检验文件状态...\n"
if check_type == "pre":
message = "\n正在检查游戏文件以确定需要安装的补丁...\n"
elif check_type == "after":
message = "\n正在检验本地文件完整性...\n"
elif check_type == "extraction":
message = "\n正在验证下载的解压文件完整性...\n"
if is_offline:
# 离线模式的消息
if check_type == "pre":
message = "\n正在检查游戏文件以确定需要安装的补丁...\n"
elif check_type == "after":
message = "\n正在检验本地文件完整性...\n"
elif check_type == "offline_verify":
message = "\n正在验证本地补丁压缩文件完整性...\n"
elif check_type == "offline_extraction":
message = "\n正在解压安装补丁文件...\n"
else:
message = "\n正在处理离线补丁文件...\n"
else:
# 在线模式的消息
if check_type == "pre":
message = "\n正在检查游戏文件以确定需要安装的补丁...\n"
elif check_type == "after":
message = "\n正在检验本地文件完整性...\n"
elif check_type == "extraction":
message = "\n正在验证下载的解压文件完整性...\n"
msg_box = msgbox_frame(f"通知 - {APP_NAME}", message)
msg_box.open()
@@ -137,49 +152,94 @@ class HashManager:
def cfg_pre_hash_compare(self, install_paths, plugin_hash, installed_status):
status_copy = installed_status.copy()
debug_mode = False
# 尝试检测是否处于调试模式
try:
from data.config import CACHE
debug_file = os.path.join(os.path.dirname(CACHE), "debug_mode.txt")
debug_mode = os.path.exists(debug_file)
except:
pass
for game_version, install_path in install_paths.items():
if not os.path.exists(install_path):
status_copy[game_version] = False
if debug_mode:
print(f"DEBUG: 哈希预检查 - {game_version} 补丁文件不存在: {install_path}")
continue
try:
expected_hash = plugin_hash.get(game_version, "")
file_hash = self.hash_calculate(install_path)
if file_hash == plugin_hash.get(game_version):
if debug_mode:
print(f"DEBUG: 哈希预检查 - {game_version}")
print(f"DEBUG: 文件路径: {install_path}")
print(f"DEBUG: 预期哈希值: {expected_hash}")
print(f"DEBUG: 实际哈希值: {file_hash}")
print(f"DEBUG: 哈希匹配: {file_hash == expected_hash}")
if file_hash == expected_hash:
status_copy[game_version] = True
else:
status_copy[game_version] = False
except Exception:
except Exception as e:
status_copy[game_version] = False
if debug_mode:
print(f"DEBUG: 哈希预检查异常 - {game_version}: {str(e)}")
return status_copy
def cfg_after_hash_compare(self, install_paths, plugin_hash, installed_status):
debug_mode = False
# 尝试检测是否处于调试模式
try:
from data.config import CACHE
debug_file = os.path.join(os.path.dirname(CACHE), "debug_mode.txt")
debug_mode = os.path.exists(debug_file)
except:
pass
file_paths = [
install_paths[game] for game in plugin_hash if installed_status.get(game)
]
hash_results = self.calculate_hashes_in_parallel(file_paths)
for game, hash_value in plugin_hash.items():
for game, expected_hash in plugin_hash.items():
if installed_status.get(game):
file_path = install_paths[game]
file_hash = hash_results.get(file_path)
if debug_mode:
print(f"DEBUG: 哈希后检查 - {game}")
print(f"DEBUG: 文件路径: {file_path}")
print(f"DEBUG: 预期哈希值: {expected_hash}")
print(f"DEBUG: 实际哈希值: {file_hash if file_hash else '计算失败'}")
if file_hash is None:
installed_status[game] = False
if debug_mode:
print(f"DEBUG: 哈希后检查失败 - 无法计算文件哈希值: {game}")
return {
"passed": False,
"game": game,
"message": f"\n无法计算 {game} 的文件哈希值,文件可能已损坏或被占用。\n"
}
if file_hash != hash_value:
if file_hash != expected_hash:
installed_status[game] = False
if debug_mode:
print(f"DEBUG: 哈希后检查失败 - 哈希值不匹配: {game}")
return {
"passed": False,
"game": game,
"message": f"\n检测到 {game} 的文件哈希值不匹配。\n"
}
if debug_mode:
print(f"DEBUG: 哈希后检查通过 - 所有文件哈希值匹配")
return {"passed": True}
class AdminPrivileges:
@@ -580,8 +640,17 @@ class HostsManager:
return False
def censor_url(text):
"""Censors URLs in a given text string."""
"""Censors URLs in a given text string, replacing them with a protection message.
Args:
text: 要处理的文本
Returns:
str: 处理后的文本URL被完全隐藏
"""
if not isinstance(text, str):
text = str(text)
# 匹配URL并替换为固定文本
url_pattern = re.compile(r'https?://[^\s/$.?#].[^\s]*')
return url_pattern.sub('***URL HIDDEN***', text)
return url_pattern.sub('***URL protection***', text)

View File

@@ -3,6 +3,15 @@ import logging
import os
from data.config import CACHE
class URLCensorFormatter(logging.Formatter):
"""自定义的日志格式化器用于隐藏日志消息中的URL"""
def format(self, record):
# 先使用原始的format方法格式化日志
formatted_message = super().format(record)
# 然后对格式化后的消息进行URL审查
return censor_url(formatted_message)
class Logger:
def __init__(self, filename, stream):
self.terminal = stream
@@ -53,7 +62,7 @@ def setup_logger(name):
console_handler.setLevel(logging.INFO)
# 创建格式器并添加到处理器
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
formatter = URLCensorFormatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)

View File

@@ -18,7 +18,8 @@ class ConfigFetchThread(QThread):
try:
if self.debug_mode:
print("--- Starting to fetch cloud config ---")
print(f"DEBUG: Requesting URL: {self.url}")
# 完全隐藏URL
print(f"DEBUG: Requesting URL: ***URL protection***")
print(f"DEBUG: Using Headers: {self.headers}")
response = requests.get(self.url, headers=self.headers, timeout=10)
@@ -26,7 +27,18 @@ class ConfigFetchThread(QThread):
if self.debug_mode:
print(f"DEBUG: Response Status Code: {response.status_code}")
print(f"DEBUG: Response Headers: {response.headers}")
print(f"DEBUG: Response Text: {response.text}")
# 解析并隐藏响应中的敏感URL
try:
response_data = response.json()
# 创建安全版本用于日志输出
safe_response = self._create_safe_config_for_logging(response_data)
print(f"DEBUG: Response Text: {json.dumps(safe_response, indent=2)}")
except:
# 如果不是JSON直接打印文本
from utils.helpers import censor_url
censored_text = censor_url(response.text)
print(f"DEBUG: Response Text: {censored_text}")
response.raise_for_status()
@@ -62,4 +74,28 @@ class ConfigFetchThread(QThread):
self.finished.emit(None, error_msg)
finally:
if self.debug_mode:
print("--- Finished fetching cloud config ---")
print("--- Finished fetching cloud config ---")
def _create_safe_config_for_logging(self, config_data):
"""创建用于日志记录的安全配置副本隐藏敏感URL
Args:
config_data: 原始配置数据
Returns:
dict: 安全的配置数据副本
"""
if not config_data or not isinstance(config_data, dict):
return config_data
# 创建深拷贝,避免修改原始数据
import copy
safe_config = copy.deepcopy(config_data)
# 隐藏敏感URL
for key in safe_config:
if isinstance(safe_config[key], dict) and "url" in safe_config[key]:
# 完全隐藏URL
safe_config[key]["url"] = "***URL protection***"
return safe_config

View File

@@ -200,7 +200,15 @@ class DownloadThread(QThread):
command.append(self.url)
print(f"即将执行的 Aria2c 命令: {' '.join(command)}")
# 创建一个安全的命令副本隐藏URL
safe_command = command.copy()
if len(safe_command) > 0:
# 替换最后一个参数URL为安全版本
url = safe_command[-1]
if isinstance(url, str) and url.startswith("http"):
safe_command[-1] = "***URL protection***"
print(f"即将执行的 Aria2c 命令: {' '.join(safe_command)}")
creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
self.process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding='utf-8', errors='replace', creationflags=creation_flags)
@@ -220,8 +228,11 @@ class DownloadThread(QThread):
else:
break
full_output.append(line)
print(line.strip())
# 处理输出行隐藏可能包含的URL
from utils.helpers import censor_url
censored_line = censor_url(line)
full_output.append(censored_line)
print(censored_line.strip())
match = progress_pattern.search(line)
if match:

View File

@@ -30,6 +30,9 @@ class IpOptimizer:
ip_txt_path = resource_path("ip.txt")
# 隐藏敏感URL
safe_url = "***URL protection***"
command = [
cst_path,
"-n", "1000", # 延迟测速线程数
@@ -39,10 +42,17 @@ class IpOptimizer:
"-dd", # 禁用下载测速
"-o"," " # 不写入结果文件
]
# 创建用于显示的安全命令副本
safe_command = command.copy()
for i, arg in enumerate(safe_command):
if arg == url:
safe_command[i] = safe_url
creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
print("--- CloudflareSpeedTest 开始执行 ---")
print(f"执行命令: {' '.join(safe_command)}")
self.process = subprocess.Popen(
command,
@@ -91,7 +101,9 @@ class IpOptimizer:
timeout_counter = 0
cleaned_line = line.strip()
# 处理输出行隐藏可能包含的URL
from utils.helpers import censor_url
cleaned_line = censor_url(line.strip())
if cleaned_line:
print(cleaned_line)
@@ -157,6 +169,9 @@ class IpOptimizer:
print(f"错误: ipv6.txt 未在资源路径中找到。")
return None
# 隐藏敏感URL
safe_url = "***URL protection***"
command = [
cst_path,
"-n", "1000", # 延迟测速线程数
@@ -166,10 +181,17 @@ class IpOptimizer:
"-dd", # 禁用下载测速
"-o", " " # 不写入结果文件
]
# 创建用于显示的安全命令副本
safe_command = command.copy()
for i, arg in enumerate(safe_command):
if arg == url:
safe_command[i] = safe_url
creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
print("--- CloudflareSpeedTest IPv6 开始执行 ---")
print(f"执行命令: {' '.join(safe_command)}")
self.process = subprocess.Popen(
command,
@@ -218,7 +240,9 @@ class IpOptimizer:
timeout_counter = 0
cleaned_line = line.strip()
# 处理输出行隐藏可能包含的URL
from utils.helpers import censor_url
cleaned_line = censor_url(line.strip())
if cleaned_line:
print(cleaned_line)