增加微信多开功能

This commit is contained in:
李志强 2026-04-07 18:54:31 +08:00
parent 3051f6ca49
commit 621d752a8d
7 changed files with 317 additions and 35 deletions

View File

@ -34,7 +34,7 @@ exe = EXE(
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx=False,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
@ -43,5 +43,5 @@ exe = EXE(
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=['logo.png'],
icon=['logo.ico'],
)

5
Readme.md Normal file
View File

@ -0,0 +1,5 @@
# 编译命令
python -m PyInstaller CleanDesktopOrganizer.spec
# 清除缓存重新打包命令
python -m PyInstaller CleanDesktopOrganizer.spec --clean

View File

@ -9,7 +9,7 @@ from ui.ball import FloatBall, BALL_SIZE
import ui.theme as theme
from db import database
__VERSION__ = "0.0.2"
__VERSION__ = "0.0.3"
# ===================== 打包兼容核心函数 =====================
def get_resource_path(relative_path):

Binary file not shown.

View File

@ -507,6 +507,7 @@ class PanelWindow(QWidget):
每项: (tooltip, qtawesome_icon, callback)
排除重启/退出"""
return [
("微信多开", "fa5b.weixin", self._open_wechat_multi),
("管理员运行 CMD", "fa5s.terminal", self._open_admin_cmd),
("管理员运行 PowerShell", "fa5b.windows", self._open_admin_powershell),
("打开默认浏览器", "fa5s.globe", self._open_default_browser),
@ -913,6 +914,11 @@ class PanelWindow(QWidget):
import webbrowser
webbrowser.open("https://www.baidu.com")
def _open_wechat_multi(self):
from ui.wechat_multi import WechatMultiDialog
dlg = WechatMultiDialog(self)
dlg.exec()
def _toggle_theme(self):
theme.set_theme("light" if theme.name() == "dark" else "dark")
self._apply_theme()

View File

@ -16,7 +16,6 @@ UPDATE_CHECK_URL = "https://api.yunzer.cn/api/softwareupgrade/check?code=niumaso
def _current_version() -> str:
"""从 main 模块取当前版本号,避免循环导入。"""
try:
import importlib
m = importlib.import_module("__main__")
@ -35,13 +34,12 @@ def _is_newer(latest: str, current: str) -> bool:
class _CheckWorker(QThread):
result = pyqtSignal(dict) # 成功:返回 data 字段
result = pyqtSignal(dict)
error = pyqtSignal(str)
def run(self):
try:
import json
import urllib.request
with urllib.request.urlopen(UPDATE_CHECK_URL, timeout=10) as resp:
body = json.loads(resp.read().decode())
if body.get("code") == 200:
@ -53,59 +51,90 @@ class _CheckWorker(QThread):
class _DownloadWorker(QThread):
progress = pyqtSignal(int) # 0-100
finished = pyqtSignal(str) # 下载完成,返回临时文件路径
progress = pyqtSignal(int)
finished = pyqtSignal(str)
error = pyqtSignal(str)
def __init__(self, url: str):
def __init__(self, url: str, dest: str):
super().__init__()
self._url = url
self._dest = dest
def run(self):
try:
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".exe")
tmp.close()
dst = tmp.name
def _reporthook(count, block_size, total_size):
if total_size > 0:
pct = min(100, int(count * block_size * 100 / total_size))
self.progress.emit(pct)
urllib.request.urlretrieve(self._url, dst, _reporthook)
urllib.request.urlretrieve(self._url, self._dest, _reporthook)
self.progress.emit(100)
self.finished.emit(dst)
self.finished.emit(self._dest)
except Exception as e:
self.error.emit(str(e))
def _replace_and_restart(new_exe: str):
"""
写一个批处理脚本等待当前进程退出 覆盖 exe 启动新版
仅在打包为 exe 时执行真正替换开发环境直接启动下载文件
onedir 模式只替换 exe 本身dll 等文件不变
PowerShell 后台脚本等待原进程退出后替换并重启完全无窗口
"""
current_exe = sys.executable if getattr(sys, "frozen", False) else None
if current_exe:
bat = tempfile.NamedTemporaryFile(delete=False, suffix=".bat", mode="w", encoding="gbk")
bat.write(f"""@echo off
ping 127.0.0.1 -n 3 >nul
move /y "{new_exe}" "{current_exe}"
start "" "{current_exe}"
del "%~f0"
""")
bat.close()
subprocess.Popen(["cmd", "/c", bat.name], creationflags=subprocess.CREATE_NO_WINDOW)
else:
# 开发环境:直接运行下载的 exe
if not getattr(sys, "frozen", False):
subprocess.Popen([new_exe])
QApplication.quit()
return
current_exe = sys.executable
pid = os.getpid()
ps_script = f"""
$pid_target = {pid}
$src = '{new_exe.replace(chr(92), chr(92)*2)}'
$dst = '{current_exe.replace(chr(92), chr(92)*2)}'
# 等待原进程退出,最多 30 秒
$waited = 0
while ((Get-Process -Id $pid_target -ErrorAction SilentlyContinue) -and $waited -lt 30) {{
Start-Sleep -Milliseconds 500
$waited += 0.5
}}
# 重试覆盖,最多 10 次
$ok = $false
for ($i = 0; $i -lt 10; $i++) {{
try {{
Copy-Item -Path $src -Destination $dst -Force
$ok = $true
break
}} catch {{
Start-Sleep -Milliseconds 800
}}
}}
if ($ok) {{
Remove-Item $src -Force -ErrorAction SilentlyContinue
Start-Process $dst
}}
"""
tmp = tempfile.NamedTemporaryFile(
delete=False, suffix=".ps1", mode="w", encoding="utf-8"
)
tmp.write(ps_script)
tmp.close()
subprocess.Popen(
[
"powershell", "-WindowStyle", "Hidden",
"-NonInteractive", "-ExecutionPolicy", "Bypass",
"-File", tmp.name,
],
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NO_WINDOW,
)
QApplication.quit()
class Updater(QObject):
"""对外接口:调用 check() 即可。"""
def __init__(self, parent=None):
super().__init__(parent)
self._parent_widget = parent
@ -141,6 +170,7 @@ class Updater(QObject):
if ret != QMessageBox.StandardButton.Yes:
return
self._url = url
self._download(url)
def _on_check_error(self, err: str):
@ -148,12 +178,19 @@ class Updater(QObject):
QMessageBox.warning(self._parent_widget, "检查更新失败", f"无法连接更新服务器:\n{err}")
def _download(self, url: str):
# 下载到原 exe 同目录,避免跨盘 move 失败
if getattr(sys, "frozen", False):
dest_dir = os.path.dirname(sys.executable)
else:
dest_dir = tempfile.gettempdir()
dest = os.path.join(dest_dir, "_update_new.exe")
self._progress = QProgressDialog("正在下载更新...", "取消", 0, 100, self._parent_widget)
self._progress.setWindowTitle("下载更新")
self._progress.setMinimumDuration(0)
self._progress.setValue(0)
self._dl_worker = _DownloadWorker(url)
self._dl_worker = _DownloadWorker(url, dest)
self._dl_worker.progress.connect(self._progress.setValue)
self._dl_worker.finished.connect(self._on_download_done)
self._dl_worker.error.connect(self._on_download_error)

234
ui/wechat_multi.py Normal file
View File

@ -0,0 +1,234 @@
"""
微信多开对话框
"""
import os
import subprocess
import tempfile
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
QPushButton, QSpinBox, QFileDialog, QTextEdit, QWidget
)
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QIcon
import ui.theme as theme
from db import database
_DB_KEY_PATH = "wechat_multi_path"
_DB_KEY_COUNT = "wechat_multi_count"
_DEFAULT_PATHS = [
r"D:\Softwares\Tencent\Weixin\Weixin.exe",
r"C:\Program Files\Tencent\WeChat\WeChat.exe",
r"C:\Program Files (x86)\Tencent\WeChat\WeChat.exe",
]
_USAGE = """使用说明:
1. 填写微信 Weixin.exe 的完整路径
点击右侧浏览按钮可以直接选择文件
2. 设置多开数量建议不超过 5
数量过多可能导致电脑卡顿
3. 点击开始多开程序会依次启动
对应数量的微信进程
4. 每个微信实例需要单独登录账号
注意微信官方不支持多开使用本功能
请自行承担相关风险
"""
def _detect_wechat() -> str:
"""尝试自动检测微信路径。"""
saved = database.get_setting(_DB_KEY_PATH, "")
if saved and os.path.isfile(saved):
return saved
for p in _DEFAULT_PATHS:
if os.path.isfile(p):
return p
return ""
class WechatMultiDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("微信多开")
self.setMinimumWidth(460)
self.setWindowFlags(
Qt.WindowType.Dialog | Qt.WindowType.FramelessWindowHint
)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self._drag_pos = None
self._build()
self._apply_theme()
def _build(self):
saved_path = _detect_wechat()
saved_count = int(database.get_setting(_DB_KEY_COUNT, "2"))
root = QVBoxLayout(self)
root.setContentsMargins(0, 0, 0, 0)
self._card = QWidget()
self._card.setObjectName("wechat_card")
card_layout = QVBoxLayout(self._card)
card_layout.setContentsMargins(20, 16, 20, 16)
card_layout.setSpacing(12)
# 标题栏
title_row = QHBoxLayout()
lbl_title = QLabel("微信多开")
lbl_title.setStyleSheet("font-size:14px; font-weight:bold;")
close_btn = QPushButton("")
close_btn.setFixedSize(24, 24)
close_btn.setStyleSheet("border:none; background:transparent; font-size:13px;")
close_btn.clicked.connect(self.reject)
title_row.addWidget(lbl_title)
title_row.addStretch()
title_row.addWidget(close_btn)
card_layout.addLayout(title_row)
# 路径
card_layout.addWidget(QLabel("微信程序路径Weixin.exe"))
path_row = QHBoxLayout()
self._path_edit = QLineEdit(saved_path)
self._path_edit.setPlaceholderText("请输入或浏览 Weixin.exe 路径")
browse_btn = QPushButton("浏览")
browse_btn.setFixedWidth(56)
browse_btn.clicked.connect(self._browse)
path_row.addWidget(self._path_edit)
path_row.addWidget(browse_btn)
card_layout.addLayout(path_row)
# 数量
count_row = QHBoxLayout()
count_row.addWidget(QLabel("多开数量:"))
self._spin = QSpinBox()
self._spin.setRange(2, 9)
self._spin.setValue(saved_count)
self._spin.setFixedWidth(70)
count_row.addWidget(self._spin)
count_row.addStretch()
card_layout.addLayout(count_row)
# 使用说明
usage = QTextEdit()
usage.setReadOnly(True)
usage.setPlainText(_USAGE)
usage.setFixedHeight(150)
usage.setStyleSheet("font-size:11px; border-radius:6px;")
card_layout.addWidget(usage)
# 按钮行
btn_row = QHBoxLayout()
btn_row.addStretch()
self._start_btn = QPushButton("开始多开")
self._start_btn.setFixedHeight(32)
self._start_btn.setMinimumWidth(90)
self._start_btn.clicked.connect(self._start)
cancel_btn = QPushButton("取消")
cancel_btn.setFixedHeight(32)
cancel_btn.setMinimumWidth(70)
cancel_btn.clicked.connect(self.reject)
btn_row.addWidget(self._start_btn)
btn_row.addWidget(cancel_btn)
card_layout.addLayout(btn_row)
root.addWidget(self._card)
def _apply_theme(self):
t = theme.current()
is_dark = theme.name() == "dark"
self._card.setStyleSheet(f"""
QWidget#wechat_card {{
background: {t['panel_bg']};
border-radius: 10px;
border: 1px solid {t['panel_border']};
}}
QLabel {{ color: {t['search_color']}; background: transparent; }}
QLineEdit {{
background: {t['search_bg']};
border: 1px solid {t['search_border']};
border-radius: 5px;
color: {t['search_color']};
padding: 4px 8px;
}}
QSpinBox {{
background: {t['search_bg']};
border: 1px solid {t['search_border']};
border-radius: 5px;
color: {t['search_color']};
padding: 2px 4px;
}}
QTextEdit {{
background: {t['search_bg']};
color: {t['search_color']};
border: 1px solid {t['search_border']};
}}
QPushButton {{
border: 1px solid {t['search_border']};
border-radius: 5px;
color: {t['btn_color']};
background: {t['search_bg']};
font-size: 12px;
padding: 0 10px;
}}
QPushButton:hover {{ background: {t['header_hover']}; }}
""")
def _browse(self):
path, _ = QFileDialog.getOpenFileName(
self, "选择 Weixin.exe", "", "可执行文件 (*.exe)"
)
if path:
self._path_edit.setText(path)
def _start(self):
exe = self._path_edit.text().strip()
count = self._spin.value()
if not exe:
self._path_edit.setPlaceholderText("⚠ 请先填写路径")
return
if not os.path.isfile(exe):
self._path_edit.setStyleSheet(
self._path_edit.styleSheet() + "border-color: red;"
)
return
# 保存设置
database.set_setting(_DB_KEY_PATH, exe)
database.set_setting(_DB_KEY_COUNT, str(count))
# 写临时 bat连续 start 多次
lines = ["@echo off"]
for _ in range(count):
lines.append(f'start "" "{exe}"')
bat_content = "\r\n".join(lines) + "\r\n"
tmp = tempfile.NamedTemporaryFile(
delete=False, suffix=".bat", mode="w", encoding="gbk"
)
tmp.write(bat_content)
tmp.close()
subprocess.Popen(
["cmd", "/c", tmp.name],
creationflags=subprocess.CREATE_NO_WINDOW
)
self.accept()
# 无边框拖动
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self._drag_pos = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
def mouseMoveEvent(self, event):
if self._drag_pos and event.buttons() & Qt.MouseButton.LeftButton:
self.move(event.globalPosition().toPoint() - self._drag_pos)
def mouseReleaseEvent(self, event):
self._drag_pos = None