-
Notifications
You must be signed in to change notification settings - Fork 57
/
Copy pathinstall_windows.py
831 lines (670 loc) · 32.3 KB
/
install_windows.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
# 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()