mirror of
https://github.com/Mzdyl/LiteLoaderQQNT_Install.git
synced 2026-10-17 05:21:19 +08:00
831 lines
32 KiB
Python
831 lines
32 KiB
Python
# coding=utf-8
|
||
import os
|
||
import sys
|
||
import time
|
||
import json
|
||
import ctypes
|
||
import winreg
|
||
import shutil
|
||
import urllib
|
||
import struct
|
||
import msvcrt
|
||
import psutil
|
||
import requests
|
||
import tempfile
|
||
import traceback
|
||
import subprocess
|
||
import tkinter as tk
|
||
from tkinter import filedialog
|
||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||
|
||
# 当前版本号
|
||
current_version = "1.18.1"
|
||
|
||
class Config:
|
||
def __init__(self, timeout=5):
|
||
self.timeout = timeout
|
||
|
||
config = Config()
|
||
|
||
# 存储反代服务器的URL
|
||
def get_github_proxy_urls():
|
||
return [
|
||
"https://ghfast.top"
|
||
"https://gh.ddlc.top",
|
||
"https://slink.ltd",
|
||
"https://cors.isteed.cc",
|
||
"https://hub.gitmirror.com",
|
||
"https://sciproxy.com",
|
||
"https://ghproxy.net",
|
||
"https://gitclone.com",
|
||
"https://hub.incept.pw",
|
||
"https://github.moeyy.xyz",
|
||
"https://dl.ghpig.top",
|
||
"https://gh-proxy.com",
|
||
"https://hub.whtrys.space",
|
||
"https://gh-proxy.ygxz.in",
|
||
"https://ghproxy.net"
|
||
]
|
||
|
||
|
||
# 设置标准输出编码为UTF-8
|
||
sys.stdout.reconfigure(encoding="utf-8")
|
||
subprocess.run("chcp 65001", shell=True)
|
||
|
||
# x64 or x86 signatures and replacements
|
||
SIG_ARM64 = bytes([0x98, 0x01, 0x00, 0x35, 0xB2, 0xED, 0xFF, 0x97])
|
||
FIX_ARM64 = bytes([0x98, 0x01, 0x00, 0x35, 0x20, 0x00, 0x80, 0xD2])
|
||
SIG_X64 = bytes([0x48, 0x89, 0xCE, 0x48, 0x8B, 0x11, 0x4C, 0x8B, 0x41, 0x08, 0x49, 0x29, 0xD0, 0x48, 0x8B, 0x49, 0x18, 0xE8])
|
||
FIX_X64 = bytes([0x48, 0x89, 0xCE, 0x48, 0x8B, 0x11, 0x4C, 0x8B, 0x41, 0x08, 0x49, 0x29, 0xD0, 0x48, 0x8B, 0x49, 0x18, 0xB8, 0x01, 0x00, 0x00, 0x00])
|
||
SIG_X86 = bytes([0x89, 0xCE, 0x8B, 0x01, 0x8B, 0x49, 0x04, 0x29, 0xC1, 0x51, 0x50, 0xFF, 0x76, 0x0C, 0xE8])
|
||
FIX_X86 = bytes([0x89, 0xCE, 0x8B, 0x01, 0x8B, 0x49, 0x04, 0x29, 0xC1, 0x51, 0x50, 0xFF, 0x76, 0x0C, 0xB8, 0x01, 0x00, 0x00, 0x00])
|
||
|
||
|
||
def scan_and_replace(buffer, pattern, replacement):
|
||
index = 0
|
||
while index < len(buffer):
|
||
index = buffer.find(pattern, index)
|
||
if index == -1:
|
||
break
|
||
buffer[index: index + len(replacement)] = replacement
|
||
print(f"Found at 0x{index:08X}")
|
||
index += len(replacement)
|
||
|
||
|
||
def get_pe_arch(pe_file):
|
||
e_lfanew_offset = 0x3C
|
||
pe_header_offset = struct.unpack("<I", pe_file[e_lfanew_offset:e_lfanew_offset + 4])[0]
|
||
machine_offset = pe_header_offset + 4
|
||
machine = struct.unpack("<H", pe_file[machine_offset:machine_offset + 2])[0]
|
||
return machine
|
||
|
||
|
||
def patch_pe_file(file_path):
|
||
# 存在 64 位系统安装 32 位 QQ 的可能,需考虑
|
||
try:
|
||
# 创建备份文件的路径
|
||
backup_path = file_path + ".bak"
|
||
|
||
# 如果备份文件已存在,覆盖它
|
||
if os.path.exists(backup_path):
|
||
os.remove(backup_path)
|
||
# print(f"已删除旧的备份文件: {backup_path}")
|
||
|
||
# 创建新的备份
|
||
os.rename(file_path, backup_path)
|
||
print(f"已将原版备份在: {backup_path}")
|
||
|
||
with open(backup_path, "rb") as file:
|
||
pe_file = bytearray(file.read())
|
||
|
||
machine = get_pe_arch(pe_file)
|
||
if machine == 0x8664: # x64
|
||
scan_and_replace(pe_file, SIG_X64, FIX_X64)
|
||
elif machine == 0x014C: # x86
|
||
scan_and_replace(pe_file, SIG_X86, FIX_X86)
|
||
elif machine == 0xAA64: # ARM64
|
||
scan_and_replace(pe_file, SIG_ARM64, FIX_ARM64)
|
||
|
||
with open(file_path, "wb") as output_file:
|
||
output_file.write(pe_file)
|
||
|
||
print("修补成功!")
|
||
except Exception as e:
|
||
print(f"发生错误: {e}")
|
||
input("按 回车键 退出。")
|
||
|
||
|
||
def get_qq_exe_path():
|
||
root = tk.Tk()
|
||
root.withdraw()
|
||
file_path = filedialog.askopenfilename(
|
||
title="选择 QQ.exe 文件", filetypes=[("Executable files", "*.exe")]
|
||
)
|
||
return file_path
|
||
|
||
|
||
def read_registry_key(hive, subkey, value_name):
|
||
try:
|
||
# 打开指定的注册表项
|
||
key = winreg.OpenKey(hive, subkey)
|
||
# 读取注册表项中指定名称的值
|
||
value, _ = winreg.QueryValueEx(key, value_name)
|
||
# 关闭注册表项
|
||
winreg.CloseKey(key)
|
||
return value
|
||
except Exception as e:
|
||
print(f"注册表读取失败: {e}")
|
||
return None
|
||
|
||
|
||
def compare_versions(version1, version2):
|
||
v1_parts = [int(part) for part in version1.split(".")]
|
||
v2_parts = [int(part) for part in version2.split(".")]
|
||
|
||
# 对比版本号的每个部分
|
||
for i in range(max(len(v1_parts), len(v2_parts))):
|
||
v1_part = v1_parts[i] if i < len(v1_parts) else 0
|
||
v2_part = v2_parts[i] if i < len(v2_parts) else 0
|
||
|
||
if v1_part < v2_part:
|
||
return False
|
||
elif v1_part > v2_part:
|
||
return True
|
||
|
||
return False # 两个版本号相等
|
||
|
||
|
||
def check_for_updates():
|
||
try:
|
||
# 获取最新版本号
|
||
latest_url = "https://github.com/Mzdyl/LiteLoaderQQNT_Install/releases/latest"
|
||
response = requests.get(latest_url, timeout=2)
|
||
latest_release = response.url.split('/')[-1] # 获取重定向后的 URL 中的版本号
|
||
|
||
if compare_versions(latest_release, current_version):
|
||
print(f"发现新版本 {latest_release}!")
|
||
# 提示用户是否下载更新
|
||
user_input = countdown_input("是否要下载更新?输入 'y' 确认,5 秒内未输入则跳过更新。","n")
|
||
|
||
if user_input == 'y':
|
||
download_url = f"https://github.com/Mzdyl/LiteLoaderQQNT_Install/releases/download/{latest_release}/install_windows.exe"
|
||
download_file(download_url, f"install_windows-{latest_release}.exe")
|
||
print("版本已更新,请重新运行最新脚本。")
|
||
input("按 回车键 退出")
|
||
sys.exit(0)
|
||
else:
|
||
print("跳过更新,继续安装。")
|
||
else:
|
||
print("当前已是最新版本,开始安装。")
|
||
except Exception as e:
|
||
print(f"检查更新阶段发生错误: {e}")
|
||
print("将跳过检查更新,继续安装")
|
||
|
||
|
||
def get_qq_path():
|
||
try:
|
||
hive, subkey, value = winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ", "UninstallString"
|
||
uninstall_string = read_registry_key(hive, subkey, value)
|
||
|
||
if not uninstall_string or not os.path.exists(uninstall_string):
|
||
raise FileNotFoundError("无法通过注册表读取 QQNT 的安装目录或路径不存在")
|
||
|
||
qq_exe_path = uninstall_string.replace("Uninstall.exe", "QQ.exe")
|
||
print(f"QQNT 的安装目录为: {qq_exe_path}")
|
||
except Exception as e:
|
||
print(e)
|
||
print("请手动选择 QQ.exe 文件 ")
|
||
qq_exe_path = get_qq_exe_path()
|
||
|
||
return qq_exe_path
|
||
|
||
|
||
def get_document_path() -> str:
|
||
try:
|
||
# 部分用户是OneDrive 路径,是否有影响
|
||
registry_hive = winreg.HKEY_CURRENT_USER
|
||
registry_subkey = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders"
|
||
registry_value_name = "Personal"
|
||
|
||
path = read_registry_key(registry_hive, registry_subkey, registry_value_name)
|
||
|
||
if path is None:
|
||
path = os.path.expanduser("~/Documents")
|
||
|
||
if path.startswith("%USERPROFILE%"):
|
||
path = path.replace("%USERPROFILE%", os.path.expanduser("~"))
|
||
|
||
except Exception as e:
|
||
print(f"获取文档路径失败: {e}")
|
||
path = os.path.expanduser("~/Documents")
|
||
|
||
return path
|
||
|
||
|
||
def can_connect(url, timeout=2):
|
||
try:
|
||
response = requests.head(url, timeout=timeout)
|
||
return response.status_code >= 200 and response.status_code < 400
|
||
except requests.exceptions.RequestException:
|
||
return False
|
||
|
||
|
||
def install_liteloader(file_path):
|
||
try:
|
||
temp_dir = tempfile.gettempdir()
|
||
download_and_extract_form_release("LiteLoaderQQNT/LiteLoaderQQNT")
|
||
# download_and_extract_from_git("LiteLoaderQQNT/LiteLoaderQQNT")
|
||
print("下载完成,开始安装 LiteLoaderQQNT")
|
||
|
||
source_dir = os.path.join(file_path, "resources", "app", "LiteLoaderQQNT")
|
||
destination_dir = os.path.join(file_path, "resources", "app", "LiteLoaderQQNT_bak")
|
||
|
||
if os.path.exists(source_dir):
|
||
try:
|
||
os.rename(source_dir, destination_dir)
|
||
print(f"已将旧版备份为: {destination_dir}")
|
||
except Exception as e1:
|
||
print(f"重命名失败,尝试使用 shutil.move() 重命名: {e1}")
|
||
try:
|
||
time.sleep(1) # 等待一秒,防止文件被锁定
|
||
shutil.move(source_dir, destination_dir)
|
||
print(f"已将旧版备份为: {destination_dir}")
|
||
except Exception as e2:
|
||
print(f"使用 shutil.move() 重命名失败: {e2}")
|
||
|
||
# print(f"移动自: {os.path.join(temp_dir, 'LiteLoaderQQNT')}")
|
||
# print(f"移动到: {source_dir}")
|
||
|
||
try:
|
||
shutil.move(os.path.join(temp_dir, "LiteLoaderQQNT"), source_dir)
|
||
except Exception as e1:
|
||
print(f"移动 LiteLoaderQQNT 失败, 尝试再次移动: {e1}")
|
||
time.sleep(1) # 等待一秒,防止文件被锁定
|
||
try:
|
||
shutil.move(os.path.join(temp_dir, "LiteLoaderQQNT"), source_dir)
|
||
except Exception as e2:
|
||
print(f"再次尝试移动失败: {e2}")
|
||
|
||
except Exception as e:
|
||
print(f"安装LL过程发生错误: {e}")
|
||
|
||
|
||
def countdown_input(prompt, default='y'):
|
||
print(prompt)
|
||
|
||
start_time = time.time()
|
||
user_input = None
|
||
while (time.time() - start_time) < config.timeout:
|
||
if msvcrt.kbhit():
|
||
user_input = msvcrt.getch().decode("utf-8").strip().lower()
|
||
break
|
||
return user_input if user_input else default
|
||
|
||
|
||
def setup_environment_and_move_files(qq_exe_path):
|
||
try:
|
||
lite_loader_profile = os.getenv("LITELOADERQQNT_PROFILE")
|
||
if lite_loader_profile:
|
||
modify_change = countdown_input(f"检测到数据目录为 {lite_loader_profile},是否修改(y/N): ", 'n')
|
||
else:
|
||
modify_change = countdown_input("检测到未设置 LITELOADERQQNT_PROFILE 环境变量,是否设置环境变量?(Y/n): ")
|
||
|
||
if modify_change == 'y':
|
||
print("默认将为你修改为用户目录下 Documents 文件夹内")
|
||
custom_path_choice = countdown_input("是否使用自定义路径?(y/N): ", 'n')
|
||
if custom_path_choice == 'y':
|
||
root = tk.Tk()
|
||
root.withdraw()
|
||
custom_path = filedialog.askdirectory(title="请选择你要设定的 LiteLoaderQQNT 数据文件")
|
||
custom_path = os.path.normpath(custom_path) # 路径转换
|
||
command = ('setx LITELOADERQQNT_PROFILE "' + custom_path + '"')
|
||
else:
|
||
default_path = get_document_path() + '\\LiteloaderQQNT'
|
||
command = ('setx LITELOADERQQNT_PROFILE "' + default_path + '"')
|
||
os.system(command)
|
||
print("注意,目前版本修改环境变量后需重启电脑 Python 才能检测到")
|
||
print("但不影响 LiteloaderQQNT 正常使用")
|
||
print("接下来尝试检查是否存在旧数据并尝试移动")
|
||
if custom_path_choice == 'y':
|
||
lite_loader_profile = custom_path
|
||
else:
|
||
lite_loader_profile = default_path
|
||
os.environ['ML_LITELOADERQQNT_TEMP'] = lite_loader_profile
|
||
|
||
source_dir = os.path.join(os.path.dirname(qq_exe_path), "resources", "app", "LiteLoaderQQNT")
|
||
folders = ["plugins", "data"]
|
||
if all(os.path.exists(os.path.join(source_dir, folder)) for folder in folders):
|
||
for folder in folders:
|
||
source_folder = os.path.join(source_dir, folder)
|
||
target_folder = os.path.join(lite_loader_profile, folder)
|
||
if os.path.exists(target_folder):
|
||
print(f"目标文件夹 {target_folder} 已存在,跳过移动操作。")
|
||
else:
|
||
shutil.move(source_folder, target_folder)
|
||
print(f"成功移动 {folder} 文件夹至 {lite_loader_profile}")
|
||
print(f"你的 LiteloaderQQNT 插件数据目录在 {lite_loader_profile}")
|
||
else:
|
||
print("已取消修改环境变量操作。")
|
||
except Exception as e:
|
||
print(f"检测并修改数据目录时发生错误: {e}")
|
||
|
||
|
||
def cleanup_old_bak(qq_exe_path):
|
||
try:
|
||
file_path = os.path.dirname(qq_exe_path)
|
||
|
||
# 访问LiteLoaderQQNT目录并更改目录和文件权限
|
||
lite_loader_qqnt_paths = [
|
||
os.path.join(file_path, "resources", "app", "LiteLoaderQQNT_bak"),
|
||
os.path.join(file_path, "resources", "app", "LiteLoaderQQNT")
|
||
]
|
||
|
||
for path in lite_loader_qqnt_paths:
|
||
change_folder_permissions(path, "everyone", "(oi)(ci)(F)")
|
||
|
||
# 删除备份文件
|
||
bak_file_path = qq_exe_path + ".bak"
|
||
if os.path.exists(bak_file_path):
|
||
os.remove(bak_file_path)
|
||
print(f"已删除备份文件: {bak_file_path}")
|
||
# else:
|
||
# print("备份文件不存在,无需删除。")
|
||
|
||
# 移除旧版备份文件夹
|
||
try:
|
||
shutil.rmtree(os.path.join(file_path, "resources", "app", "LiteLoaderQQNT_bak"), ignore_errors=True)
|
||
except Exception as e:
|
||
print(f"移除旧版备份失败,尝试再次移除: {e}")
|
||
os.system(f'del "{os.path.join(file_path, "resources", "app", "LiteLoaderQQNT_bak")}" /F')
|
||
|
||
except Exception as e:
|
||
print(f"移除旧版备份时发生错误: {e}")
|
||
|
||
|
||
def patch_index_js(file_path):
|
||
try:
|
||
app_launcher_path = os.path.join(file_path, "resources", "app", "app_launcher")
|
||
os.chdir(app_launcher_path)
|
||
print("开始修补 index.js…")
|
||
index_path = os.path.join(app_launcher_path, "index.js")
|
||
# 备份原文件
|
||
print("已将旧版文件备份为 index.js.bak ")
|
||
bak_index_path = index_path + ".bak"
|
||
shutil.copyfile(index_path, bak_index_path)
|
||
with open(index_path, "w", encoding="utf-8") as f:
|
||
f.write(f"require('{os.path.join(file_path, 'resources', 'app', 'LiteLoaderQQNT').replace(os.sep, '/')}');\n")
|
||
f.write("require('./launcher.node').load('external_index', module);")
|
||
except Exception as e:
|
||
print(f"修补 index.js 时发生错误: {e}")
|
||
|
||
|
||
def create_launcher_js(file_path, version_path, launcher_name="ml_install.js"):
|
||
try:
|
||
# 设置 app_launcher 目录路径
|
||
app_launcher_path = os.path.join(version_path, 'resources', 'app', 'app_launcher')
|
||
os.makedirs(app_launcher_path, exist_ok=True) # 确保目录存在
|
||
|
||
# 新建 launcher 文件
|
||
launcher_js_path = os.path.join(app_launcher_path, launcher_name)
|
||
print(f"开始创建 {launcher_js_path}...")
|
||
|
||
with open(launcher_js_path, "w", encoding="utf-8") as f:
|
||
f.write(f"require(String.raw`{os.path.join(file_path, 'resources', 'app', 'LiteLoaderQQNT').replace(os.sep, '/')}`);\n")
|
||
|
||
print(f"已创建 {launcher_name} 文件")
|
||
return launcher_js_path
|
||
|
||
except Exception as e:
|
||
print(f"创建 launcher 文件时发生错误: {e}")
|
||
return None
|
||
|
||
def patch_package_json(version_path, launcher_name="ml_install.js"):
|
||
try:
|
||
app_launcher_path = os.path.join(version_path, 'resources', 'app')
|
||
print("开始修补 package.json…")
|
||
|
||
package_path = os.path.join(app_launcher_path, "package.json")
|
||
# 备份原文件
|
||
print("已将旧版文件备份为 package.json.bak ")
|
||
bak_package_path = package_path + ".bak"
|
||
shutil.copyfile(package_path, bak_package_path)
|
||
|
||
# 读取并修改 package.json
|
||
with open(package_path, 'r', encoding='utf-8') as file:
|
||
data = json.load(file)
|
||
|
||
# 修改 main 字段的值为新创建的 launcher 文件路径
|
||
data["main"] = f"./app_launcher/{launcher_name}"
|
||
|
||
# 将修改后的内容写回 package.json 文件
|
||
with open(package_path, 'w', encoding='utf-8') as file:
|
||
json.dump(data, file, indent=4, ensure_ascii=False)
|
||
|
||
print(f'"main" 字段已修改为: {data["main"]}')
|
||
|
||
except Exception as e:
|
||
print(f"修补 package.json 时发生错误: {e}")
|
||
|
||
|
||
def patch(file_path):
|
||
try:
|
||
# 获取LITELOADERQQNT_PROFILE和ML_LITELOADERQQNT_TEMP环境变量的值
|
||
lite_loader_profile = os.getenv("LITELOADERQQNT_PROFILE")
|
||
lite_loader_temp = os.getenv("ML_LITELOADERQQNT_TEMP")
|
||
|
||
# 如果环境变量不存在,则使用默认路径
|
||
default_path = os.path.join(file_path, "resources", "app", "LiteLoaderQQNT", "plugins")
|
||
if lite_loader_profile:
|
||
plugin_path = os.path.join(lite_loader_profile, "plugins")
|
||
elif lite_loader_temp:
|
||
print(
|
||
"未能检测到LITELOADERQQNT_PROFILE,但检测到安装器临时环境变量,猜测你已设置环境变量,使用安装器临时环境变量")
|
||
plugin_path = os.path.join(lite_loader_temp, "plugins")
|
||
else:
|
||
print("未能检测到LITELOADERQQNT_PROFILE,使用默认路径")
|
||
plugin_path = default_path
|
||
|
||
# 检查并创建插件目录
|
||
if not os.path.exists(plugin_path):
|
||
os.makedirs(plugin_path)
|
||
print(f"插件目录 {plugin_path} 不存在,已创建。")
|
||
|
||
# 打印或使用 plugin_path 变量
|
||
print(f"你的插件路径是 {plugin_path}")
|
||
print("赋予插件目录和插件数据目录完全控制权(解决部分插件权限问题)")
|
||
change_folder_permissions(plugin_path, "everyone", "(oi)(ci)(F)")
|
||
plugin_data_dir = os.path.join(os.path.dirname(plugin_path), "data")
|
||
change_folder_permissions(plugin_data_dir, "everyone", "(oi)(ci)(F)")
|
||
|
||
except Exception as e:
|
||
print(f"发生错误: {e}")
|
||
print(f"大概率不影响安装,安装继续")
|
||
|
||
|
||
def check_and_kill_qq(process_name):
|
||
try:
|
||
for proc in psutil.process_iter():
|
||
# 检查进程是否与指定的名称匹配
|
||
if proc.name() == process_name:
|
||
print(f"找到进程 {process_name},将于3秒后尝试关闭...")
|
||
time.sleep(3)
|
||
proc.kill()
|
||
print(f"进程 {process_name} 已关闭。")
|
||
except Exception as e:
|
||
print(f"关闭进程 {process_name} 时发生: {e},无影响,继续执行")
|
||
|
||
|
||
def change_folder_permissions(folder_path, user, permissions):
|
||
try:
|
||
cmd = ["icacls", folder_path, "/grant", f"{user}:{permissions}", "/t"]
|
||
subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL)
|
||
# print(f"成功修改文件夹 {folder_path} 的权限。")
|
||
except subprocess.CalledProcessError as e:
|
||
print(f"修改文件夹权限时出错: {e}")
|
||
|
||
|
||
def install_plugin_store(file_path):
|
||
try:
|
||
# 获取LITELOADERQQNT_PROFILE环境变量的值
|
||
lite_loader_profile = os.getenv('LITELOADERQQNT_PROFILE')
|
||
lite_loader_temp = os.getenv("ML_LITELOADERQQNT_TEMP")
|
||
|
||
if not lite_loader_profile:
|
||
if lite_loader_temp:
|
||
print("未检测到环境变量 LITELOADERQQNT_PROFILE,但检测到安装器临时环境变量,猜测你已设置环境变量,使用安装器临时环境变量")
|
||
plugin_path = os.path.join(lite_loader_temp, 'plugins')
|
||
else:
|
||
print("环境变量 LITELOADERQQNT_PROFILE 未设置,使用默认路径")
|
||
plugin_path = os.path.join(file_path, "resources", "app", "LiteLoaderQQNT", "plugins")
|
||
else:
|
||
plugin_path = os.path.join(lite_loader_profile, 'plugins')
|
||
|
||
existing_destination_path = os.path.join(plugin_path, 'list-viewer')
|
||
|
||
temp_dir = tempfile.gettempdir()
|
||
|
||
if not os.path.exists(existing_destination_path) or not os.path.exists(os.path.join(existing_destination_path, 'main', 'index.js')):
|
||
if not os.path.exists(existing_destination_path):
|
||
os.makedirs(plugin_path, exist_ok=True)
|
||
else:
|
||
print("检测到已安装插件商店可能存在问题,即将重装")
|
||
shutil.rmtree(existing_destination_path)
|
||
print("更新和安装插件请使用 release 版本")
|
||
print("非 release 版本可能导致 QQ 无法正常启动")
|
||
|
||
# 下载并解压插件
|
||
download_and_extract_form_release("ltxhhz/LL-plugin-list-viewer")
|
||
# print(f"移动自: {os.path.join(temp_dir, 'list-viewer')}")
|
||
# print(f"移动到: {existing_destination_path}")
|
||
shutil.move(os.path.join(temp_dir, "list-viewer"), plugin_path)
|
||
else:
|
||
print("检测到已安装插件商店,不再重新安装")
|
||
|
||
except Exception as e:
|
||
print(f"安装插件商店发生错误: {e}\n请尝试手动安装")
|
||
|
||
|
||
def check_proxy(proxy):
|
||
try:
|
||
proxy_url = f"{proxy}/https://github.com"
|
||
response = requests.head(proxy_url, timeout=5)
|
||
if response.ok:
|
||
return proxy
|
||
except requests.exceptions.RequestException:
|
||
pass
|
||
return None
|
||
|
||
|
||
def get_working_proxy():
|
||
proxies = get_github_proxy_urls()
|
||
with ThreadPoolExecutor(max_workers=len(proxies)) as executor:
|
||
future_to_proxy = {executor.submit(check_proxy, proxy): proxy for proxy in proxies}
|
||
for future in as_completed(future_to_proxy):
|
||
result = future.result()
|
||
if result is not None:
|
||
return result
|
||
return None
|
||
|
||
def get_download_url(url: str) -> str:
|
||
if can_connect(url):
|
||
return url
|
||
proxy = get_working_proxy()
|
||
if not proxy:
|
||
raise ValueError("无可用代理")
|
||
return f"{proxy}/{url}"
|
||
|
||
def download_file(url_or_path: str, filepath: str, timeout: int = 10):
|
||
try:
|
||
# 检查是否为本地文件路径
|
||
if os.path.exists(url_or_path):
|
||
print(f"使用本地文件路径: {url_or_path}")
|
||
shutil.copy(url_or_path, filepath)
|
||
return
|
||
if not url_or_path.startswith(('http://', 'https://')):
|
||
raise ValueError(f"无效的路径或 URL: {url_or_path}")
|
||
|
||
download_url = get_download_url(url_or_path)
|
||
print(f"当前使用的下载链接: {download_url}")
|
||
|
||
try:
|
||
# 使用 urlopen 方法来设置超时
|
||
with urllib.request.urlopen(download_url, timeout=timeout) as response:
|
||
with open(filepath, 'wb') as out_file:
|
||
out_file.write(response.read())
|
||
return
|
||
except urllib.error.URLError as e:
|
||
print(f"下载失败,错误信息: {e}\n尝试再次进行下载")
|
||
download_url = get_download_url(url_or_path)
|
||
print(f"当前使用的下载链接: {download_url}")
|
||
|
||
# 再次尝试下载文件
|
||
with urllib.request.urlopen(download_url, timeout=timeout) as response:
|
||
with open(filepath, 'wb') as out_file:
|
||
out_file.write(response.read())
|
||
|
||
except Exception as e:
|
||
print(f"下载过程中发生错误: {e}")
|
||
external_data_path = get_external_data_path()
|
||
if external_data_path:
|
||
# print(f"使用内嵌版本,路径{external_data_path}")
|
||
print(f"使用内嵌版本")
|
||
|
||
filename = os.path.basename(filepath)
|
||
filename = os.path.splitext(filename)[0]
|
||
|
||
found = False
|
||
for file in os.listdir(external_data_path):
|
||
if file.startswith(f"{filename}-") and file.endswith(".zip"):
|
||
# 找到符合条件的内嵌文件
|
||
fallback_path = os.path.join(external_data_path, file) # 更新路径为实际文件
|
||
found = True
|
||
break
|
||
if found:
|
||
shutil.copy(fallback_path, filepath)
|
||
print(f"已将内嵌版本文件复制到: {filepath}")
|
||
else:
|
||
raise ValueError(f"内嵌文件未找到: {fallback_path}")
|
||
else:
|
||
download_url = input("无法访问 GitHub 且无可用代理,请手动输入下载地址或本地文件路径(如 "
|
||
"https://mirror.ghproxy.com/https://github.com/Mzdyl/LiteLoaderQQNT_Install"
|
||
"/archive/master.zip 或 C:\\path\\to\\file.zip ):")
|
||
if not download_url:
|
||
raise ValueError("未提供有效的下载地址或本地文件路径")
|
||
download_file(download_url, filepath)
|
||
|
||
|
||
def get_latest_version(file_path):
|
||
"""
|
||
获取最新的版本目录。
|
||
|
||
:param file_path: QQ.exe 的安装目录路径
|
||
:return: 最新版本目录名称
|
||
:raises FileNotFoundError: 如果无法找到 versions 目录或版本文件夹
|
||
"""
|
||
versions_dir = os.path.join(file_path, 'versions')
|
||
if not os.path.isdir(versions_dir):
|
||
raise FileNotFoundError(f"无法找到 versions 目录: {versions_dir}")
|
||
|
||
# 获取所有版本目录名称
|
||
version_names = [d for d in os.listdir(versions_dir) if os.path.isdir(os.path.join(versions_dir, d))]
|
||
if not version_names:
|
||
raise FileNotFoundError("在 versions 目录下未找到任何版本文件夹")
|
||
|
||
# 假设版本号格式为 'x.x.x-xxxxx',通过排序选择最新版本
|
||
latest_version = sorted(version_names, reverse=True)[0]
|
||
print(f"检测到最新版本目录: {latest_version}")
|
||
|
||
return latest_version
|
||
|
||
|
||
def download_and_extract_form_release(repos: str):
|
||
temp_dir = tempfile.gettempdir()
|
||
# print(f"临时目录:{temp_dir}")
|
||
|
||
cached_names = {
|
||
"ltxhhz/LL-plugin-list-viewer": "list-viewer.zip",
|
||
"LiteLoaderQQNT/LiteLoaderQQNT": "LiteLoaderQQNT.zip"
|
||
}
|
||
|
||
if repos not in cached_names:
|
||
print("仓库名称无效")
|
||
return
|
||
|
||
filename = cached_names[repos]
|
||
zip_path = os.path.join(temp_dir, filename)
|
||
|
||
try:
|
||
# 获取网站上的最新版本号
|
||
latest_version = get_latest_tag(repos)
|
||
print(f"网站上的最新版本: {latest_version}")
|
||
|
||
# 获取内置版本号
|
||
|
||
internal_version = get_internal_version(filename)
|
||
print(f"内置版本: {internal_version}")
|
||
|
||
if latest_version == internal_version:
|
||
print(f"内置版本与最新版本一致,使用内置版本")
|
||
filename = os.path.splitext(filename)[0]
|
||
extract_dir = os.path.join(temp_dir, filename.split(".")[0])
|
||
shutil.unpack_archive(os.path.join(get_external_data_path(), f"{filename}-{internal_version}.zip"), extract_dir)
|
||
return
|
||
except Exception as e:
|
||
print(f"从GitHub获取版本信息 {repos} 时发生错误: {e}")
|
||
|
||
print(f"下载最新版本")
|
||
download_url = f"https://github.com/{repos}/releases/latest/download/{filename}"
|
||
download_file(download_url, zip_path)
|
||
extract_dir = os.path.join(temp_dir, filename.split(".")[0])
|
||
shutil.unpack_archive(zip_path, extract_dir)
|
||
|
||
|
||
def download_and_extract_from_git(repos: str):
|
||
temp_dir = tempfile.gettempdir()
|
||
print(f"临时目录:{temp_dir}")
|
||
|
||
cached_names = {
|
||
"ltxhhz/LL-plugin-list-viewer": "list-viewer.zip",
|
||
"LiteLoaderQQNT/LiteLoaderQQNT": "LiteLoaderQQNT.zip"
|
||
}
|
||
|
||
if repos not in cached_names:
|
||
print("仓库名称无效")
|
||
return
|
||
|
||
filename = cached_names[repos]
|
||
git_url = f"https://github.com/{repos}/archive/refs/heads/main.zip"
|
||
zip_path = os.path.join(temp_dir, filename)
|
||
|
||
try:
|
||
print(f"下载最新 Git 版本的 {repos}")
|
||
download_file(git_url, zip_path)
|
||
extract_dir = os.path.join(temp_dir, filename.split(".")[0])
|
||
shutil.unpack_archive(zip_path, extract_dir)
|
||
for item in os.listdir(extract_dir):
|
||
item_path = os.path.join(extract_dir, item)
|
||
if os.path.isdir(item_path):
|
||
for sub_item in os.listdir(item_path):
|
||
shutil.move(os.path.join(item_path, sub_item), extract_dir)
|
||
os.rmdir(item_path)
|
||
except Exception as e:
|
||
print(f"Git 版下载并解压 {repos} 时发生错误: {e}")
|
||
raise
|
||
|
||
|
||
def get_external_data_path():
|
||
# 兼容 PyInstaller
|
||
if hasattr(sys, '_MEIPASS'):
|
||
return sys._MEIPASS # PyInstaller 打包后的临时文件路径
|
||
# 兼容 Nuitka
|
||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||
# 查找以 "LiteLoaderQQNT-" 开头,".zip" 结尾的文件
|
||
for file in os.listdir(base_dir):
|
||
if file.startswith("LiteLoaderQQNT-") and file.endswith(".zip"):
|
||
return base_dir # 如果找到符合条件的文件,返回当前路径
|
||
return None
|
||
|
||
|
||
def get_latest_tag(repo):
|
||
url = f"https://api.github.com/repos/{repo}/releases"
|
||
response = requests.get(url)
|
||
response.raise_for_status() # 检查请求是否成功
|
||
releases = response.json()
|
||
if releases:
|
||
return releases[0]['tag_name']
|
||
return None
|
||
|
||
|
||
def get_internal_version(filename):
|
||
""" 获取内置版本号,假设内置版本文件命名为 {filename}-{version}.zip """
|
||
filename = os.path.splitext(filename)[0]
|
||
external_data_path = get_external_data_path()
|
||
|
||
# 检查路径是否存在
|
||
if not external_data_path:
|
||
raise FileNotFoundError("无法找到外部数据路径。")
|
||
|
||
# 查找符合命名格式的文件
|
||
for file in os.listdir(external_data_path):
|
||
if file.startswith(f"{filename}-") and file.endswith(".zip"):
|
||
# 从文件名中提取版本号
|
||
version = file.split('-')[-1].replace('.zip', '')
|
||
return version
|
||
|
||
return None # 如果没有找到匹配的内置版本文件,则返回 None
|
||
|
||
|
||
|
||
def main():
|
||
try:
|
||
# 检查命令行参数,决定是否启用静默安装
|
||
if '--silent' in sys.argv:
|
||
config.timeout = 0 # 静默安装时将超时时间设置为 0
|
||
|
||
# 检测是否在 GitHub Actions 中运行
|
||
github_actions = os.getenv("GITHUB_ACTIONS", False)
|
||
|
||
if not ctypes.windll.shell32.IsUserAnAdmin():
|
||
print("推荐使用管理员运行")
|
||
|
||
qq_exe_path = get_qq_path()
|
||
file_path = os.path.dirname(qq_exe_path)
|
||
|
||
skip_update_file = os.path.join(file_path, "SKIP_UPDATE")
|
||
if os.path.exists(skip_update_file):
|
||
print("检测到 SKIP_UPDATE 文件,跳过更新")
|
||
else:
|
||
check_for_updates()
|
||
|
||
check_and_kill_qq("QQ.exe")
|
||
if not github_actions:
|
||
cleanup_old_bak(qq_exe_path)
|
||
setup_environment_and_move_files(qq_exe_path)
|
||
else:
|
||
cleanup_old_bak(qq_exe_path)
|
||
|
||
qq_file_size_bytes = os.path.getsize(qq_exe_path)
|
||
qq_file_size_mb = qq_file_size_bytes / (1024 * 1024)
|
||
if qq_file_size_mb < 10:
|
||
print("QQ大小小于 10MB,判断为新版")
|
||
latest_version = get_latest_version(file_path)
|
||
version_path = os.path.join(file_path, "versions", latest_version)
|
||
qq_dll_path = os.path.join(version_path, 'QQNT.dll')
|
||
create_launcher_js(file_path, version_path)
|
||
patch_package_json(version_path)
|
||
else:
|
||
print("QQ大小大于 10MB,判断为旧版")
|
||
patch_index_js(file_path)
|
||
|
||
if os.path.exists(os.path.join(file_path, "dbghelp.dll")):
|
||
print("检测到dbghelp.dll,推测你已修补QQ,跳过修补")
|
||
else:
|
||
if qq_file_size_mb < 10:
|
||
patch_pe_file(qq_dll_path)
|
||
else:
|
||
patch_pe_file(qq_exe_path)
|
||
|
||
install_liteloader(file_path)
|
||
patch(file_path)
|
||
|
||
print("LiteLoaderQQNT 安装完成!接下来进行插件列表安装")
|
||
install_plugin_store(file_path)
|
||
|
||
if not github_actions:
|
||
print("如果安装过程中没有提示发生错误")
|
||
print("但 QQ 设置界面没有 LiteLoaderQQNT")
|
||
print("请检查已安装过的插件")
|
||
print("插件错误会导致 LiteLoaderQQNT 无法正常启动")
|
||
|
||
print("按 回车键 退出…")
|
||
input("如有问题请截图安装界面反馈")
|
||
|
||
except Exception as e:
|
||
print(f"发生错误: {e}")
|
||
print(traceback.format_exc())
|
||
input("按 回车键 退出。")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|
||
|