吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 2846|回复: 74
收起左侧

[Python 原创] Python代码跨平台打包工具v7.0.0

  [复制链接]
By:伟 发表于 2026-8-2 19:43
本帖最后由 hrh123 于 2026-8-2 21:18 编辑

看到昨天论坛上有一个人发了python打包的源码,我下载过来试了一下会报错。然后进行了修复。我在自己电脑上测试,所有功能全部正常。
软件https://wm5211.lanzouu.com/iny1l4076slc
密码:52pj image.png image.png



#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Python脚本打包工具 - PyQt6完整版 """
import sys, json, time, ast, shutil, subprocess, threading, multiprocessing
import tempfile, webbrowser, fnmatch, glob, ctypes, platform as sys_platform
import urllib.request, zipfile, traceback
from pathlib import Path
from datetime import datetime
from collections import Counter
import os, re, stat,unicodedata,textwrap,difflib,io,functools,psutil,datetime
from contextlib import redirect_stdout, redirect_stderr
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *   
try:
    from PyQt6.QtMultimedia import QMediaPlayer
    from PyQt6.QtMultimediaWidgets import QVideoWidget
    MEDIA_AVAILABLE = True
except (ImportError, AttributeError):
    MEDIA_AVAILABLE = False
VERSION = "7.0.0"
BUILD_DATE = datetime.datetime.now().strftime("%Y-%m-%d")
AUTHOR = "wcj6376"
# ==================== 常量定义 ====================
MIRROR = "https://pypi.tuna.tsinghua.edu.cn/simple"
# 国内镜像源列表
MIRRORS = ["https://pypi.tuna.tsinghua.edu.cn/simple","https://mirrors.aliyun.com/pypi/simple/","https://pypi.mirrors.ustc.edu.cn/simple/"]
STANDARD_LIBS = frozenset({
    'abc','argparse','array','ast','asyncio','atexit','base64','bdb','binascii',
    'bisect','builtins','bz2','calendar','cgi','cgitb','chunk','cmath','cmd',
    'code','codecs','codeop','collections','colorsys','compileall','concurrent',
    'configparser','contextlib','contextvars','copy','copyreg','csv','ctypes',
    'dataclasses','datetime','dbm','decimal','difflib','dis','email','encodings',
    'enum','faulthandler','fnmatch','fractions','ftplib','functools','fcntl','gc',
    'getopt','getpass','gettext','glob','graphlib','gzip','hashlib','heapq',
    'hmac','html','http','idlelib','imaplib','imghdr','importlib','inspect',
    'io','ipaddress','itertools','json','keyword','linecache','locale','logging',
    'lzma','mailbox','mailcap','marshal','math','mimetypes','mmap','modulefinder','msvcrt',
    'multiprocessing','netrc','nis','nntplib','numbers','operator','optparse',
    'os','ossaudiodev','pathlib','pdb','pickle','pickletools','pipes','pkgutil',
    'platform','plistlib','poplib','posix','posixpath','pprint','profile','pstats',
    'pty','pwd','py_compile','pyclbr','pydoc','queue','quopri','random','re',
    'readline','reprlib','resource','rlcompleter','runpy','sched','secrets',
    'select','selectors','shelve','shlex','shutil','signal','site','smtpd',
    'smtplib','sndhdr','socket','socketserver','spwd','sqlite3','ssl','stat',
    'statistics','string','stringprep','struct','subprocess','sunau','symtable',
    'sys','sysconfig','syslog','tabnanny','tarfile','telnetlib','tempfile',
    'termios','test','textwrap','threading','time','timeit','tkinter','token',
    'tokenize','trace','traceback','tracemalloc','tty','turtle','types','typing',
    'unicodedata','unittest','urllib','uu','uuid','venv','warnings','wave',
    'weakref','webbrowser','winreg','wsgiref','xml','xmlrpc','zipapp','zipfile',
    'zipimport','zlib','_thread','__future__','zoneinfo','tomllib','typing_extensions'
})
MODULE_TO_PACKAGE = {
    'cv2':'opencv-python','PIL':'Pillow','skimage':'scikit-image','sklearn':'scikit-learn',
    'bs4':'beautifulsoup4','yaml':'PyYAML','Image':'Pillow','ImageDraw':'Pillow',
    'pyautogui':'PyAutoGUI','wx':'wxPython','qtpy':'QtPy','PySide2':'PySide2',
    'PySide6':'PySide6','PyQt5':'PyQt5','PyQt6':'PyQt6','dateutil':'python-dateutil',
    'dotenv':'python-dotenv','jwt':'PyJWT','lxml':'lxml','OpenGL':'PyOpenGL',
    'redis':'redis','requests':'requests','selenium':'selenium','sqlalchemy':'SQLAlchemy',
    'matplotlib':'matplotlib','numpy':'numpy','pandas':'pandas','scipy':'scipy',
    'torch':'torch','tensorflow':'tensorflow','flask':'Flask','django':'Django',
    'fastapi':'fastapi','tornado':'tornado','aiohttp':'aiohttp','grpc':'grpcio',
    'protobuf':'protobuf','pydantic':'pydantic','typer':'typer','rich':'rich',
    'click':'click','jinja2':'Jinja2','markupsafe':'MarkupSafe','werkzeug':'Werkzeug',
    'itsdangerous':'itsdangerous','win32com': 'pywin32','LibreHardwareMonitor': 'PyLibreHardwareMonitor',
}
EXCLUDE_PACKAGES = frozenset({
    '_pytest',
    'astroid', 'asttokens', 'autopep8',
    'backcall', 'black', 'build',
    'charset_normalizer', 'coverage', 'Cython', 'cython',
    'debugpy', 'decorator', 'distribute',
    'executing',
    'fancycompleter', 'flake8',
    'ipykernel', 'ipython', 'ipywidgets', 'isort',
    'jedi', 'jupyter', 'jupyter_client', 'jupyter_core', 'jupyterlab',
    'matplotlib-inline', 'mccabe', 'mock', 'module', 'mypy', 'mypy_extensions', 'mypyc',
    'nbconvert', 'nbformat', 'nose', 'notebook','numpy',
    'packaging', 'pathspec', 'pdbpp', 'pep517', 'pip', 'pkg_resources', 'platformdirs',
    'pluggy', 'prompt_toolkit', 'ptpython', 'pure_eval', 'py', 'pycodestyle', 'pyflakes',
    'pygments', 'pyi_hooks', 'pyi_hooks_contrib', 'pyinstaller', 'pyinstaller-hooks-contrib',
    'pylint', 'pyproject_hooks', 'pywin32_ctypes', 'pytest','pywin32'
    'qtconsole',
    'setuptools', 'stack_data',
    'test', 'tests', 'tox', 'traitlets', 'typed_ast', 'typeshed_client',
    'unittest2',
    'venv', 'virtualenv',
    'wcwidth', 'wheel', 'wmctrl','win32', 'win32con'
    'yaml', 'pyyaml',
})
class PythonInstallWorker(QThread):
    """后台静默安装 Python - 完全异步版"""
    finished_signal = pyqtSignal(bool, str)
    def __init__(self):
        super().__init__()
        self._cancel = False
        self._progress = 0

    def cancel(self):
        self._cancel = True

    def run(self):
        import urllib.request
        import ssl
        import tempfile
        import os
        import subprocess
        import time
        import sys
        try:
            version = "3.12.10"
            temp_dir = tempfile.gettempdir()
            has_python, version_str = check_python_installed()
            if has_python:
                self.finished_signal.emit(True, f"Python {version_str} 已安装")
                return
            if sys.platform == 'win32':
                filename = f"python-{version}-amd64.exe"
                mirrors = [
                    f"https://www.python.org/ftp/python/{version}/python-{version}-amd64.exe",
                    f"https://mirrors.tuna.tsinghua.edu.cn/python/{version}/python-{version}-amd64.exe",
                    f"https://mirrors.aliyun.com/python/{version}/python-{version}-amd64.exe",
                    f"https://mirrors.ustc.edu.cn/python/{version}/python-{version}-amd64.exe",
                ]
            elif sys.platform == 'darwin':
                filename = f"python-{version}-macos11.pkg"
                mirrors = [
                    f"https://www.python.org/ftp/python/{version}/python-{version}-macos11.pkg",
                    f"https://mirrors.tuna.tsinghua.edu.cn/python/{version}/python-{version}-macos11.pkg",
                ]
            else:
                self.finished_signal.emit(False, "Linux请使用包管理器安装Python")
                return
            installer_path = os.path.join(temp_dir, filename)
            ssl_context = ssl.create_default_context()
            ssl_context.check_hostname = False
            ssl_context.verify_mode = ssl.CERT_NONE
            downloaded = False
            last_error = ""
            for i, url in enumerate(mirrors):
                if self._cancel:
                    self.finished_signal.emit(False, "已取消")
                    return
                try:
                    self.finished_signal.emit(False, f"下载中... 尝试镜像 {i+1}/{len(mirrors)}")
                    req = urllib.request.Request(url, headers={
                        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
                    })
                    with urllib.request.urlopen(req, context=ssl_context, timeout=60) as response:
                        total_size = int(response.headers.get('content-length', 0))
                        downloaded_size = 0
                        if os.path.exists(installer_path):
                            if total_size > 0 and os.path.getsize(installer_path) == total_size:
                                downloaded = True
                                break
                        with open(installer_path, 'wb') as f:
                            while not self._cancel:
                                chunk = response.read(8192)
                                if not chunk:
                                    break
                                f.write(chunk)
                                downloaded_size += len(chunk)
                                if total_size > 0:
                                    progress = int(downloaded_size * 100 / total_size)
                                    if progress != self._progress:
                                        self._progress = progress
                                        self.finished_signal.emit(False, f"下载中... {progress}%")
                    if os.path.exists(installer_path):
                        file_size = os.path.getsize(installer_path)
                        if file_size > 5 * 1024 * 1024:  
                            downloaded = True
                            self.finished_signal.emit(False, f"下载完成 ({file_size // 1024 // 1024}MB)")
                            break
                        else:
                            os.remove(installer_path)
                            last_error = f"文件太小 ({file_size} bytes)"
                except Exception as e:
                    last_error = str(e)
                    continue
            if self._cancel:
                self.finished_signal.emit(False, "已取消")
                return
            if not downloaded:
                self.finished_signal.emit(False, f"下载失败: {last_error}")
                return
            self.finished_signal.emit(False, "安装中... 请稍候")
            if sys.platform == 'win32':
                cmd = [
                    installer_path,
                    '/quiet',
                    'InstallAllUsers=1',
                    'PrependPath=1',
                    'Include_doc=0',
                    'Include_tcltk=0',
                    'Include_test=0',
                    'Include_tools=0',
                    'Include_pip=1',
                    'Include_setuptools=1',
                    'Include_symbols=0',
                    'Include_debug=0',
                    'InstallLauncherAllUsers=1',
                ]
            elif sys.platform == 'darwin':
                cmd = ['sudo', 'installer', '-pkg', installer_path, '-target', '/']
            else:
                self.finished_signal.emit(False, "不支持的操作系统")
                return
            process = subprocess.Popen(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
                creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
            )
            timeout_count = 0
            while process.poll() is None:
                if self._cancel:
                    process.terminate()
                    self.finished_signal.emit(False, "已取消")
                    return
                timeout_count += 1
                if timeout_count > 600:  
                    process.terminate()
                    self.finished_signal.emit(False, "安装超时")
                    return
                if timeout_count % 30 == 0:
                    progress = min(95, 50 + timeout_count // 6)
                    self.finished_signal.emit(False, f"安装中... {progress}%")
                self.msleep(500)
            try:
                os.remove(installer_path)
            except:
                pass
            if process.returncode == 0:
                self.finished_signal.emit(False, "验证安装...")
                time.sleep(3)  
                for attempt in range(5):
                    has_python, version_str = check_python_installed()
                    if has_python:
                        self.finished_signal.emit(True, f"Python {version_str} 安装成功!")
                        return
                    time.sleep(2)
                self.finished_signal.emit(False, "安装完成但验证失败,请手动安装")
            else:
                self.finished_signal.emit(False, f"安装失败 (返回码: {process.returncode})")
        except Exception as e:
            import traceback
            self.finished_signal.emit(False, f"错误: {str(e)}")

def check_python_installed():
    """检测系统是否安装了 Python"""
    import subprocess
    import sys
    import os
    import shutil
    import glob
    IS_FROZEN = getattr(sys, 'frozen', False)

    def is_valid_system_python(path):
        if not path or not os.path.exists(path):
            return False
        if IS_FROZEN:
            try:
                if os.path.samefile(path, sys.executable):
                    return False
            except:
                if path.lower() == sys.executable.lower():
                    return False
        path_lower = path.lower()
        if '_mei' in path_lower or 'onefile_' in path_lower or 'nuitka' in path_lower:
            return False
        try:
            if sys.platform == 'win32':
                result = subprocess.run(
                    [path, '--version'],
                    capture_output=True, text=True, timeout=3,
                    creationflags=subprocess.CREATE_NO_WINDOW
                )
            else:
                result = subprocess.run(
                    [path, '--version'],
                    capture_output=True, text=True, timeout=3
                )
            if result.returncode == 0:
                output = result.stdout + result.stderr
                return 'Python' in output
        except:
            pass
        return False

    def get_python_version(path):
        """获取Python版本"""
        try:
            if sys.platform == 'win32':
                result = subprocess.run(
                    [path, '--version'],
                    capture_output=True, text=True, timeout=3,
                    creationflags=subprocess.CREATE_NO_WINDOW
                )
            else:
                result = subprocess.run(
                    [path, '--version'],
                    capture_output=True, text=True, timeout=3
                )
            if result.returncode == 0:
                output = result.stdout.strip() or result.stderr.strip()
                return output
        except:
            pass
        return None
    if sys.platform == 'win32':
        try:
            result = subprocess.run(
                ['py', '--version'],
                capture_output=True, text=True, timeout=3,
                creationflags=subprocess.CREATE_NO_WINDOW
            )
            if result.returncode == 0:
                version = result.stdout.strip() or result.stderr.strip()
                return True, version
        except:
            pass
        try:
            result = subprocess.run(
                ['py', '-c', 'import sys; print(sys.executable)'],
                capture_output=True, text=True, timeout=3,
                creationflags=subprocess.CREATE_NO_WINDOW
            )
            if result.returncode == 0:
                py_path = result.stdout.strip()
                if py_path and is_valid_system_python(py_path):
                    version = get_python_version(py_path)
                    if version:
                        return True, version
        except:
            pass
        try:
            result = subprocess.run(
                ['where', 'python'],
                capture_output=True, text=True, timeout=3,
                creationflags=subprocess.CREATE_NO_WINDOW
            )
            if result.returncode == 0:
                for line in result.stdout.strip().split('\n'):
                    py_path = line.strip()
                    if is_valid_system_python(py_path):
                        version = get_python_version(py_path)
                        if version:
                            return True, version
        except:
            pass
        try:
            import winreg
            for root in [winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE]:
                try:
                    key = winreg.OpenKey(root, r'Software\Python\PythonCore')
                    i = 0
                    while True:
                        try:
                            version_key = winreg.EnumKey(key, i)
                            if version_key.startswith('3.'):
                                try:
                                    install_key = winreg.OpenKey(key, fr'{version_key}\InstallPath')
                                    install_path, _ = winreg.QueryValueEx(install_key, '')
                                    if install_path:
                                        py_path = os.path.join(install_path, 'python.exe')
                                        if os.path.exists(py_path) and is_valid_system_python(py_path):
                                            version = get_python_version(py_path)
                                            if version:
                                                return True, version
                                except:
                                    pass
                            i += 1
                        except WindowsError:
                            break
                except:
                    pass
        except:
            pass
    for cmd in ['python3', 'python']:
        py_path = shutil.which(cmd)
        if py_path and is_valid_system_python(py_path):
            version = get_python_version(py_path)
            if version:
                return True, version
    if sys.platform == 'win32':
        username = os.environ.get('USERNAME', '')
        search_patterns = [
            r'C:\Python3*',
            r'C:\Python3*\python.exe',
            rf'C:\Users\{username}\AppData\Local\Programs\Python\Python3*\python.exe',
            r'C:\Program Files\Python3*\python.exe',
            r'C:\Program Files (x86)\Python3*\python.exe',
        ]
        for pattern in search_patterns:
            for path in glob.glob(pattern):
                if os.path.isfile(path) and path.endswith('python.exe'):
                    if is_valid_system_python(path):
                        version = get_python_version(path)
                        if version:
                            return True, version
                elif os.path.isdir(path):
                    exe_path = os.path.join(path, 'python.exe')
                    if os.path.exists(exe_path) and is_valid_system_python(exe_path):
                        version = get_python_version(exe_path)
                        if version:
                            return True, version
    elif sys.platform == 'darwin':
        paths_to_check = [
            '/usr/local/bin/python3',
            '/usr/bin/python3',
            '/opt/homebrew/bin/python3',
            '/Library/Frameworks/Python.framework/Versions/3.*/bin/python3',
        ]
        for pattern in paths_to_check:
            for path in glob.glob(pattern):
                if os.path.exists(path) and is_valid_system_python(path):
                    version = get_python_version(path)
                    if version:
                        return True, version
    else:
        paths_to_check = [
            '/usr/bin/python3',
            '/usr/local/bin/python3',
            '/usr/bin/python',
            '/opt/python3/bin/python3',
        ]
        for path in paths_to_check:
            if os.path.exists(path) and is_valid_system_python(path):
                version = get_python_version(path)
                if version:
                    return True, version
    return False, None

def ensure_python_on_startup():
    """启动时检测 Python"""
    import sys
    import os
    if not getattr(sys, 'frozen', False):
        return True
    has_python, version = check_python_installed()
    if has_python:
        print(f"[Main] 检测到系统Python: {version}")
        return True
    print("[Main] 未检测到系统Python,后台静默安装...")
    if sys.platform == "win32":
        try:
            import ctypes
            if not ctypes.windll.shell32.IsUserAnAdmin():
                print("[Main] 警告: 非管理员模式,安装可能失败")
        except:
            pass
    from PyQt6.QtWidgets import QApplication
    app = QApplication.instance()
    if app is None:
        app = QApplication(sys.argv)
        app.setStyle('Fusion')
    from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QProgressBar, QPushButton
    from PyQt6.QtCore import QTimer, Qt
    tip_widget = QWidget(None, Qt.WindowType.Window | Qt.WindowType.WindowStaysOnTopHint)
    tip_widget.setWindowTitle("安装 Python")
    tip_widget.setFixedSize(380, 140)
    tip_widget.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
    layout = QVBoxLayout(tip_widget)
    layout.setSpacing(8)
    status_label = QLabel("⏳ 正在后台静默安装 Python 3.12.10...")
    status_label.setStyleSheet("font-size: 13px; font-weight: bold;")
    status_label.setWordWrap(True)
    layout.addWidget(status_label)
    progress_bar = QProgressBar()
    progress_bar.setRange(0, 100)
    progress_bar.setValue(0)
    progress_bar.setTextVisible(True)
    layout.addWidget(progress_bar)
    btn_layout = QHBoxLayout()
    btn_layout.addStretch()
    hide_btn = QPushButton("最小化到托盘")
    hide_btn.setFixedWidth(100)
    btn_layout.addWidget(hide_btn)
    cancel_btn = QPushButton("取消")
    cancel_btn.setFixedWidth(60)
    btn_layout.addWidget(cancel_btn)
    layout.addLayout(btn_layout)
    tip_widget.show()
    worker = PythonInstallWorker()
    install_success = False

    def on_progress(success, msg):
        nonlocal install_success
        if success:
            install_success = True
            status_label.setText("✅ " + msg)
            progress_bar.setValue(100)
            hide_btn.setEnabled(False)
            cancel_btn.setEnabled(False)
            cancel_btn.setText("重启")
            cancel_btn.clicked.connect(lambda: restart_app())
            QTimer.singleShot(3000, tip_widget.close)
        else:
            if "下载" in msg or "安装" in msg:
                status_label.setText("⏳ " + msg)
                import re
                match = re.search(r'(\d+)%', msg)
                if match:
                    progress_bar.setValue(int(match.group(1)))
            else:
                status_label.setText("⏳ " + msg)
                cancel_btn.setText("关闭")
                cancel_btn.clicked.connect(tip_widget.close)

    def on_hide():
        """最小化到托盘或隐藏窗口"""
        tip_widget.hide()
        print("[Main] Python安装已在后台继续...")

    def on_cancel():
        """取消安装"""
        if worker.isRunning():
            worker.cancel()
            worker.wait()
        tip_widget.close()

    def restart_app():
        """重启程序"""
        import subprocess
        import sys
        tip_widget.close()
        subprocess.Popen([sys.executable] + sys.argv)
        sys.exit(0)
    worker.finished_signal.connect(on_progress)
    hide_btn.clicked.connect(on_hide)
    cancel_btn.clicked.connect(on_cancel)
    worker.start()
    return True

def patch_subprocess_hide_window():
    """Monkey Patch: 所有 subprocess 调用默认隐藏 cmd 窗口(Windows)"""
    import subprocess
    import sys
    if sys.platform == "win32":
        _orig_run = subprocess.run
        _orig_popen = subprocess.Popen
        _orig_call = subprocess.call
        _orig_check_call = subprocess.check_call
        _orig_check_output = subprocess.check_output
        CREATE_NO_WINDOW = 0x08000000
        def _patched_run(*args, **kwargs):
            if 'creationflags' not in kwargs:
                kwargs['creationflags'] = CREATE_NO_WINDOW
            return _orig_run(*args, **kwargs)
        def _patched_popen(*args, **kwargs):
            if 'creationflags' not in kwargs:
                kwargs['creationflags'] = CREATE_NO_WINDOW
            return _orig_popen(*args, **kwargs)
        def _patched_call(*args, **kwargs):
            if 'creationflags' not in kwargs:
                kwargs['creationflags'] = CREATE_NO_WINDOW
            return _orig_call(*args, **kwargs)
        def _patched_check_call(*args, **kwargs):
            if 'creationflags' not in kwargs:
                kwargs['creationflags'] = CREATE_NO_WINDOW
            return _orig_check_call(*args, **kwargs)
        def _patched_check_output(*args, **kwargs):
            if 'creationflags' not in kwargs:
                kwargs['creationflags'] = CREATE_NO_WINDOW
            return _orig_check_output(*args, **kwargs)
        subprocess.run = _patched_run
        subprocess.Popen = _patched_popen
        subprocess.call = _patched_call
        subprocess.check_call = _patched_check_call
        subprocess.check_output = _patched_check_output

def get_short_path(self, path):
    """获取 Windows 短路径(8.3格式),如果失败则返回原路径"""
    if sys.platform != 'win32':
        return path
    try:
        import ctypes
        GetShortPathName = ctypes.windll.kernel32.GetShortPathNameW
        buffer_len = GetShortPathName(path, None, 0)
        if buffer_len == 0:
            return path
        buffer = ctypes.create_unicode_buffer(buffer_len)
        GetShortPathName(path, buffer, buffer_len)
        return buffer.value if buffer.value else path
    except Exception:
        return path

def show_msg(parent, title, text, timeout=0, buttons='ok'):
    """
    通用消息框(PyQt6)调用示例:
        show_msg(self, "提示", "操作完成")
        show_msg(self, "完成", "打包成功!", 3)
        if show_msg(self, "确认", "是否继续?", 3, 'yes_no'):
            self.do_continue()   # 点击"是" 或 超时 → 执行
        else:
            print("取消")        # 点击"否" → 跳过
        if show_msg(self, "提示", "确定要删除吗?", 5, 'ok_cancel'):
            self.do_delete()     # 点击"确定" 或 超时 → 执行
        else:
            print("取消删除")
        show_msg(None, "后台任务", "完成!", 2)
        QMessageBox.critical(self, "错误", "打包失败!")
    """
    from PyQt6.QtWidgets import QMessageBox
    from PyQt6.QtCore import QTimer
    btn_map = {
        'ok': QMessageBox.StandardButton.Ok,
        'ok_cancel': QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel,
        'yes_no': QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
    }
    msg = QMessageBox(parent)
    msg.setWindowTitle(title)
    msg.setText(text)
    msg.setIcon(QMessageBox.Icon.Information)
    msg.setStandardButtons(btn_map.get(buttons, QMessageBox.StandardButton.Ok))
    if timeout > 0:
        remaining = timeout
        msg.setText(f"{text}\n\n⏱️ {remaining}秒后自动...")
        timer = QTimer(msg)
        def update():
            nonlocal remaining
            remaining -= 1
            if remaining > 0:
                msg.setText(f"{text}\n\n⏱️ {remaining}秒后自动...")
            else:
                timer.stop()
        timer.timeout.connect(update)
        timer.start(1000)
        QTimer.singleShot(timeout * 1000, msg.accept)
    result = msg.exec()
    if buttons == 'ok':
        return True
    elif buttons == 'ok_cancel':
        return result == QMessageBox.StandardButton.Ok
    elif buttons == 'yes_no':
        return result == QMessageBox.StandardButton.Yes
    return False

class PythonHighlighter(QSyntaxHighlighter):
    """Python 语法高亮器"""
    def __init__(self, parent=None):
        super().__init__(parent)
        keyword_format = QTextCharFormat()
        keyword_format.setForeground(QColor("#569CD6"))
        keyword_format.setFontWeight(QFont.Weight.Bold)
        keywords = [
            'def', 'class', 'import', 'from', 'return', 'if', 'elif', 'else', 'for',
            'while', 'try', 'except', 'finally', 'with', 'as', 'pass', 'break',
            'continue', 'lambda', 'yield', 'assert', 'raise', 'del', 'global',
            'nonlocal', 'True', 'False', 'None', 'and', 'or', 'not', 'is', 'in',
            'async', 'await', '__init__', '__name__', '__main__', '__file__'
        ]
        self.rules = []
        for kw in keywords:
            pattern = QRegularExpression(r'\b' + kw + r'\b')
            self.rules.append((pattern, keyword_format))
        string_format = QTextCharFormat()
        string_format.setForeground(QColor("#CE9178"))
        self.rules.append((QRegularExpression(r'"[^"\\]*(\\.[^"\\]*)*"'), string_format))
        self.rules.append((QRegularExpression(r"'[^'\\]*(\\.[^'\\]*)*'"), string_format))
        comment_format = QTextCharFormat()
        comment_format.setForeground(QColor("#6A9955"))
        self.rules.append((QRegularExpression(r'#.*'), comment_format))
        number_format = QTextCharFormat()
        number_format.setForeground(QColor("#B5CEA8"))
        self.rules.append((QRegularExpression(r'\b[0-9]+\b'), number_format))

    def highlightBlock(self, text):
        for pattern, fmt in self.rules:
            iterator = pattern.globalMatch(text)
            while iterator.hasNext():
                match = iterator.next()
                self.setFormat(match.capturedStart(), match.capturedLength(), fmt)

class CodePreviewDialog(QDialog):
    """代码修复预览对话框 - 左右双栏对比"""
    skip_preview = False
    def __init__(self, parent, original_content, new_content, changes, file_path, backup_path):
        super().__init__(parent)
        self.setWindowTitle("代码修复预览")
        self.setModal(True)
        # ===== 获取屏幕尺寸并设置窗口大小 =====
        screen = QApplication.primaryScreen()
        screen_geometry = screen.availableGeometry()
        screen_width = screen_geometry.width()
        screen_height = screen_geometry.height()
        window_width = int(screen_width * 0.9)
        window_height = int(screen_height * 0.8)
        self.setMinimumSize(int(screen_width * 0.5), int(screen_height * 0.5))
        self.resize(window_width, window_height)
        self.original_content = original_content
        self.new_content = new_content
        self.changes = changes
        self.file_path = file_path
        self.backup_path = backup_path
        self.left_modified = False
        self.right_modified = False
        self._setup_ui()
        self._show_diff()
        self.setStyleSheet("")

    def _setup_ui(self):
        layout = QVBoxLayout(self)
        # 顶部信息
        info = QHBoxLayout()
        info.addWidget(QLabel(f"📄 文件: {os.path.basename(self.file_path)}"))
        info.addStretch()
        info.addWidget(QLabel(f"📝 修改: {len(self.changes)} 处"))
        layout.addLayout(info)
        # 对比区域
        splitter = QSplitter(Qt.Orientation.Horizontal)
        # === 左侧:原始代码 ===
        left_widget = QWidget()
        left_layout = QVBoxLayout(left_widget)
        left_title = QLabel("🔴 原始代码")
        left_title.setStyleSheet("font-weight: bold; color: #c0392b; font-size: 12px;")
        left_layout.addWidget(left_title)
        self.left_edit = QPlainTextEdit()
        self.left_edit.setFont(QFont("Consolas", 11))
        self.left_edit.setStyleSheet("""
            QPlainTextEdit {
                background-color: #fdf2f2;
                color: #1a0000;
                border: 2px solid #e74c3c;
                border-radius: 6px;
                padding: 8px;
                selection-background-color: #ffcccc;
            }
        """)
        self.left_edit.textChanged.connect(lambda: self._on_edit("left"))
        left_layout.addWidget(self.left_edit)
        splitter.addWidget(left_widget)
        # === 右侧:修复后代码 ===
        right_widget = QWidget()
        right_layout = QVBoxLayout(right_widget)
        right_title = QLabel("🟢 修复后代码")
        right_title.setStyleSheet("font-weight: bold; color: #27ae60; font-size: 12px;")
        right_layout.addWidget(right_title)
        self.right_edit = QPlainTextEdit()
        self.right_edit.setFont(QFont("Consolas", 11))
        self.right_edit.setStyleSheet("""
            QPlainTextEdit {
                background-color: #f0faf0;
                color: #001a00;
                border: 2px solid #2ecc71;
                border-radius: 6px;
                padding: 8px;
                selection-background-color: #a8e6cf;
            }
        """)
        self.right_edit.textChanged.connect(lambda: self._on_edit("right"))
        right_layout.addWidget(self.right_edit)
        splitter.addWidget(right_widget)
        splitter.setSizes([450, 450])
        layout.addWidget(splitter, stretch=1)
        # 修改列表
        layout.addWidget(QLabel("📋 修改详情:"))
        self.changes_list = QListWidget()
        self.changes_list.setMaximumHeight(120)
        self.changes_list.setStyleSheet("""
            QListWidget {
                background-color: #f8f9fa;
                color: #212529;
                border: 1px solid #dee2e6;
                border-radius: 4px;
                font-size: 11px;
            }
            QListWidget::item {
                padding: 4px 8px;
                border-bottom: 1px solid #f0f0f0;
            }
            QListWidget::item:selected {
                background-color: #007bff;
                color: white;
            }
        """)
        for change in self.changes:
            self.changes_list.addItem(change)
        layout.addWidget(self.changes_list)
        # 底部按钮
        btn_layout = QHBoxLayout()
        btn_layout.setSpacing(8)
        left_btns = QHBoxLayout()
        self.save_left_btn = QPushButton("💾 保存左侧")
        self.save_left_btn.setEnabled(False)
        self.save_left_btn.setStyleSheet("background: #e74c3c; color: white; font-weight: bold; padding: 6px 14px; border-radius: 4px;")
        self.save_left_btn.clicked.connect(lambda: self._save_side("left"))
        left_btns.addWidget(self.save_left_btn)
        self.revert_left_btn = QPushButton("↩️ 还原左侧")
        self.revert_left_btn.setStyleSheet("background: #95a5a6; color: white; padding: 6px 14px; border-radius: 4px;")
        self.revert_left_btn.clicked.connect(lambda: self._revert_side("left"))
        left_btns.addWidget(self.revert_left_btn)
        btn_layout.addLayout(left_btns)
        btn_layout.addStretch()
        right_btns = QHBoxLayout()
        self.save_right_btn = QPushButton("💾 保存右侧")
        self.save_right_btn.setEnabled(False)
        self.save_right_btn.setStyleSheet("background: #27ae60; color: white; font-weight: bold; padding: 6px 14px; border-radius: 4px;")
        self.save_right_btn.clicked.connect(lambda: self._save_side("right"))
        right_btns.addWidget(self.save_right_btn)
        self.revert_right_btn = QPushButton("↩️ 还原右侧")
        self.revert_right_btn.setStyleSheet("background: #95a5a6; color: white; padding: 6px 14px; border-radius: 4px;")
        self.revert_right_btn.clicked.connect(lambda: self._revert_side("right"))
        right_btns.addWidget(self.revert_right_btn)
        btn_layout.addLayout(right_btns)
        btn_layout.addStretch()
        self.btn_apply = QPushButton("✅ 应用修复")
        self.btn_apply.setStyleSheet("background: #2ecc71; color: white; font-weight: bold; padding: 8px 24px; border-radius: 6px; font-size: 12px;")
        self.btn_apply.clicked.connect(self.accept)
        btn_layout.addWidget(self.btn_apply)
        self.btn_cancel = QPushButton("❌ 取消")
        self.btn_cancel.setStyleSheet("background: #e74c3c; color: white; padding: 8px 24px; border-radius: 6px;")
        self.btn_cancel.clicked.connect(self.reject)
        btn_layout.addWidget(self.btn_cancel)
        layout.addLayout(btn_layout)
        # 备份提示
        bottom_info = QHBoxLayout()
        bottom_info.addWidget(QLabel("💡 备份文件 (.bak.py) 将保留,可手动恢复"))
        bottom_info.addStretch()
        layout.addLayout(bottom_info)
        self.status_label = QLabel("就绪")
        self.status_label.setStyleSheet("border: 1px solid #ddd; padding: 2px 8px; background: #f8f9fa; border-radius: 4px;")
        layout.addWidget(self.status_label)
        self._update_status()

    def _show_diff(self):
        self.left_edit.setPlainText(self.original_content)
        self.right_edit.setPlainText(self.new_content)

    def _on_edit(self, side):
        if side == "left":
            self.left_modified = True
            self.save_left_btn.setEnabled(True)
        else:
            self.right_modified = True
            self.save_right_btn.setEnabled(True)
        self._update_status()

    def _save_side(self, side):
        content = self.left_edit.toPlainText() if side == "left" else self.right_edit.toPlainText()
        with open(self.backup_path, 'w', encoding='utf-8') as f:
            f.write(content)
        if side == "left":
            self.left_modified = False
            self.save_left_btn.setEnabled(False)
        else:
            self.right_modified = False
            self.save_right_btn.setEnabled(False)
        self._update_status()
        self.safe_log(f"✅ 已保存{'左侧' if side == 'left' else '右侧'}到备份")

    def _revert_side(self, side):
        if side == "left":
            self.left_edit.setPlainText(self.original_content)
            self.left_modified = False
            self.save_left_btn.setEnabled(False)
        else:
            self.right_edit.setPlainText(self.new_content)
            self.right_modified = False
            self.save_right_btn.setEnabled(False)
        self._update_status()

    def _update_status(self):
        self.status_label.setText(f"左侧: {'已修改' if self.left_modified else '未修改'} | 右侧: {'已修改' if self.right_modified else '未修改'}")

    def _log(self, msg):
        if self.parent() and hasattr(self.parent(), 'safe_log'):
            self.parent().safe_log(msg)

    def accept(self):
        """用户确认应用修复"""
        self.safe_log(f"✅ 修复已应用,备份保留: {os.path.basename(self.backup_path)}")
        super().accept()

    def reject(self):
        """用户取消,从备份还原(备份保留)"""
        if os.path.exists(self.backup_path):
            try:
                shutil.copy2(self.backup_path, self.file_path)
                self.safe_log(f"↩️ 已从备份还原: {os.path.basename(self.file_path)}")
                self.safe_log(f"💡 备份保留: {os.path.basename(self.backup_path)}")
            except Exception as e:
                self.safe_log(f"⚠️ 还原失败: {e}")
        super().reject()

class CodeCompareDialog(QDialog):
    """增强版代码对比 - 4窗口 + 函数列表 + 同步滚动 + 模糊匹配 + 互覆盖"""
    def __init__(self, parent, left_file, right_file):
        super().__init__(parent)
        self.setWindowTitle(f"代码对比 - {os.path.basename(left_file)} ↔ {os.path.basename(right_file)}")
        self.resize(1200, 800)
        self.setMinimumSize(800, 600)
        self.setModal(True)
        self.left_file = left_file
        self.right_file = right_file
        self.view_mode = "func"
        self.current_func = None
        self.sync_mode = False
        self.left_modified = False
        self.right_modified = False
        self.highlight_active = False
        self.fuzzy_match_enabled = False  
        self._load_files()
        self._extract_functions()
        self._setup_ui()
        self._display_func_mode()
        self._update_status()
        self.sync_mode = True
        self._bind_sync()
        self.sync_btn.setText("🔗 滚动 (开)")
        self.sync_btn.setStyleSheet("background: #2ecc71; color: white;")
        if self.common_funcs:
            self._select_func(self.common_funcs[0])

    def _load_files(self):
        with open(self.left_file, 'r', encoding='utf-8') as f:
            self.left_lines = f.readlines()
        with open(self.right_file, 'r', encoding='utf-8') as f:
            self.right_lines = f.readlines()
        self.left_original = self.left_lines.copy()
        self.right_original = self.right_lines.copy()

    def _extract_functions(self):
        self.left_funcs = {}
        self.right_funcs = {}

        def extract(lines, func_dict):
            i = 0
            while i < len(lines):
                line = lines.strip()
                if line.startswith('def '):
                    match = re.match(r'def\s+(\w+)\s*\(', line)
                    if match:
                        name = match.group(1)
                        start = i
                        j = i + 1
                        indent = len(lines) - len(lines.lstrip())
                        while j < len(lines):
                            if lines[j].strip():
                                curr_indent = len(lines[j]) - len(lines[j].lstrip())
                                if curr_indent <= indent and lines[j].strip().startswith('def '):
                                    break
                            j += 1
                        end = j - 1
                        body = ''.join(lines[start:end+1])
                        func_dict[name] = {'start': start, 'end': end, 'body': body}
                        i = j
                    else:
                        i += 1
                else:
                    i += 1
        extract(self.left_lines, self.left_funcs)
        extract(self.right_lines, self.right_funcs)
        self.common_funcs = sorted(set(self.left_funcs.keys()) & set(self.right_funcs.keys()))

    def showEvent(self, event):
        """窗口显示时调整大小"""
        super().showEvent(event)
        try:
            screen = QApplication.primaryScreen().availableGeometry()
            width = int(screen.width() * 0.8)
            height = int(screen.height() * 0.8)
            self.resize(width, height)
            self.setMinimumSize(int(screen.width() * 0.6), int(screen.height() * 0.4))
        except:
            pass

    def _setup_ui(self):
        layout = QVBoxLayout(self)
        # 工具栏
        toolbar = QWidget()
        tb = QHBoxLayout(toolbar)
        tb.setContentsMargins(0,0,0,0)
        self.func_mode_btn = QPushButton("函数")
        self.func_mode_btn.setStyleSheet("background: #2ecc71; color: white;")
        self.func_mode_btn.clicked.connect(self._switch_to_func_mode)
        tb.addWidget(self.func_mode_btn)
        self.line_mode_btn = QPushButton("逐行")
        self.line_mode_btn.setStyleSheet("background: #95a5a6; color: white;")
        self.line_mode_btn.clicked.connect(self._switch_to_line_mode)
        tb.addWidget(self.line_mode_btn)
        tb.addWidget(QLabel("|"))
        # ===== 模糊匹配开关 =====
        self.fuzzy_match_cb = QCheckBox("🔍 模糊")
        self.fuzzy_match_cb.setChecked(False)
        self.fuzzy_match_cb.setToolTip("开启后,精确匹配不到的函数会尝试相似度匹配")
        self.fuzzy_match_cb.stateChanged.connect(self._refresh_func_mode)
        tb.addWidget(self.fuzzy_match_cb)
        tb.addWidget(QLabel("|"))
        # ===== 互覆盖按钮 =====
        self.copy_left_to_right_btn = QPushButton("左→右")
        self.copy_left_to_right_btn.setStyleSheet("background: #e74c3c; color: white;")
        self.copy_left_to_right_btn.setToolTip("将左侧选中的行覆盖到右侧对应位置")
        self.copy_left_to_right_btn.clicked.connect(self._copy_selected_left_to_right)
        tb.addWidget(self.copy_left_to_right_btn)
        self.copy_right_to_left_btn = QPushButton("右→左")
        self.copy_right_to_left_btn.setStyleSheet("background: #3498db; color: white;")
        self.copy_right_to_left_btn.setToolTip("将右侧选中的行覆盖到左侧对应位置")
        self.copy_right_to_left_btn.clicked.connect(self._copy_selected_right_to_left)
        tb.addWidget(self.copy_right_to_left_btn)
        tb.addWidget(QLabel("|"))
        self.highlight_btn = QPushButton("高亮")
        self.highlight_btn.clicked.connect(self._on_highlight_clicked)
        self.highlight_btn.setStyleSheet("background: #e67e22; color: white;")
        self.highlight_btn.setToolTip("在当前模式下高亮显示差异")
        tb.addWidget(self.highlight_btn)
        # === 清除高亮按钮(点击后变色) ===
        self.clear_highlight_btn = QPushButton("清除")
        self.clear_highlight_btn.clicked.connect(self._clear_highlights)
        self.clear_highlight_btn.setStyleSheet("background: #95a5a6; color: white;")
        self.clear_highlight_btn.setToolTip("清除所有差异高亮")
        tb.addWidget(self.clear_highlight_btn)
        tb.addWidget(QLabel("|"))
        self.sync_btn = QPushButton("🔗 滚动")
        self.sync_btn.clicked.connect(self._toggle_sync)
        self.sync_btn.setStyleSheet("background: #3498db; color: white;")
        tb.addWidget(self.sync_btn)
        tb.addStretch()
        self.save_left_btn = QPushButton("💾 存左")
        self.save_left_btn.setEnabled(False)
        self.save_left_btn.clicked.connect(lambda: self._save_side("left"))
        tb.addWidget(self.save_left_btn)
        self.revert_left_btn = QPushButton("↩️ 还左")
        self.revert_left_btn.clicked.connect(lambda: self._revert_side("left"))
        tb.addWidget(self.revert_left_btn)
        self.save_right_btn = QPushButton("💾 存右")
        self.save_right_btn.setEnabled(False)
        self.save_right_btn.clicked.connect(lambda: self._save_side("right"))
        tb.addWidget(self.save_right_btn)
        self.revert_right_btn = QPushButton("↩️ 还右")
        self.revert_right_btn.clicked.connect(lambda: self._revert_side("right"))
        tb.addWidget(self.revert_right_btn)
        tb.addWidget(QLabel("|"))
        self.export_btn = QPushButton("导出")
        self.export_btn.clicked.connect(self._export_report)
        self.export_btn.setStyleSheet("background: #1abc9c; color: white;")
        tb.addWidget(self.export_btn)
        self.reload_btn = QPushButton("加载")
        self.reload_btn.clicked.connect(self._reload_files)
        self.reload_btn.setStyleSheet("background: #f39c12; color: white;")
        tb.addWidget(self.reload_btn)
        self.select_btn = QPushButton("选择")
        self.select_btn.clicked.connect(self._select_files)
        self.select_btn.setStyleSheet("background: #9b59b6; color: white;")
        tb.addWidget(self.select_btn)
        layout.addWidget(toolbar)
        # 主分割:左侧函数列表 + 右侧4窗口
        main_splitter = QSplitter(Qt.Orientation.Horizontal)
        # 左侧函数列表
        left_frame = QWidget()
        left_layout = QVBoxLayout(left_frame)
        left_layout.addWidget(QLabel("📋 函数列表"))
        self.search_edit = QLineEdit()
        self.search_edit.setPlaceholderText("搜索函数...")
        self.search_edit.textChanged.connect(self._filter_func_list)
        left_layout.addWidget(self.search_edit)
        self.func_list = QListWidget()
        self.func_list.itemClicked.connect(self._on_func_clicked)
        left_layout.addWidget(self.func_list)
        main_splitter.addWidget(left_frame)
        main_splitter.setSizes([200, 800])
        # 右侧4窗口
        right_widget = QWidget()
        right_layout = QVBoxLayout(right_widget)
        right_layout.setContentsMargins(0,0,0,0)
        top_bottom = QSplitter(Qt.Orientation.Vertical)
        # 预览区
        preview_splitter = QSplitter(Qt.Orientation.Horizontal)
        left_preview_frame = QFrame()
        left_preview_frame.setFrameStyle(QFrame.Shape.StyledPanel)
        left_preview_layout = QVBoxLayout(left_preview_frame)
        left_preview_layout.addWidget(QLabel("📄 源文件 (预览)"))
        self.left_preview = QPlainTextEdit()
        self.left_preview.setReadOnly(True)
        self.left_preview.setFont(QFont("Consolas", 10))
        self.left_preview.setStyleSheet("background: #1e1e1e; color: #d4d4d4;")
        PythonHighlighter(self.left_preview.document())
        left_preview_layout.addWidget(self.left_preview)
        preview_splitter.addWidget(left_preview_frame)
        right_preview_frame = QFrame()
        right_preview_frame.setFrameStyle(QFrame.Shape.StyledPanel)
        right_preview_layout = QVBoxLayout(right_preview_frame)
        right_preview_layout.addWidget(QLabel("📄 编译文件 (预览)"))
        self.right_preview = QPlainTextEdit()
        self.right_preview.setReadOnly(True)
        self.right_preview.setFont(QFont("Consolas", 10))
        self.right_preview.setStyleSheet("background: #1e1e1e; color: #d4d4d4;")
        PythonHighlighter(self.right_preview.document())
        right_preview_layout.addWidget(self.right_preview)
        preview_splitter.addWidget(right_preview_frame)
        preview_splitter.setSizes([400,400])
        top_bottom.addWidget(preview_splitter)
        # 编辑区
        edit_splitter = QSplitter(Qt.Orientation.Horizontal)
        left_edit_frame = QFrame()
        left_edit_frame.setFrameStyle(QFrame.Shape.StyledPanel)
        left_edit_layout = QVBoxLayout(left_edit_frame)
        self.left_edit_title = QLabel("✏️ 源函数: 未选中")
        left_edit_layout.addWidget(self.left_edit_title)
        self.left_edit = QPlainTextEdit()
        self.left_edit.setFont(QFont("Consolas", 10))
        self.left_edit.setStyleSheet("background: #2b2b2b; color: #d4d4d4;")
        self.left_edit.textChanged.connect(lambda: self._on_edit("left"))
        PythonHighlighter(self.left_edit.document())
        left_edit_layout.addWidget(self.left_edit)
        edit_splitter.addWidget(left_edit_frame)
        right_edit_frame = QFrame()
        right_edit_frame.setFrameStyle(QFrame.Shape.StyledPanel)
        right_edit_layout = QVBoxLayout(right_edit_frame)
        self.right_edit_title = QLabel("✏️ 编译函数: 未选中")
        right_edit_layout.addWidget(self.right_edit_title)
        self.right_edit = QPlainTextEdit()
        self.right_edit.setFont(QFont("Consolas", 10))
        self.right_edit.setStyleSheet("background: #2b2b2b; color: #d4d4d4;")
        self.right_edit.textChanged.connect(lambda: self._on_edit("right"))
        PythonHighlighter(self.right_edit.document())
        right_edit_layout.addWidget(self.right_edit)
        edit_splitter.addWidget(right_edit_frame)
        edit_splitter.setSizes([400,400])
        top_bottom.addWidget(edit_splitter)
        top_bottom.setSizes([400,400])
        right_layout.addWidget(top_bottom)
        main_splitter.addWidget(right_widget)
        layout.addWidget(main_splitter, stretch=1)
        # 状态栏
        status_frame = QFrame()
        status_frame.setFrameStyle(QFrame.Shape.StyledPanel)
        status_layout = QHBoxLayout(status_frame)
        self.status_label = QLabel("就绪")
        status_layout.addWidget(self.status_label)
        status_layout.addStretch()
        self.status_func = QLabel("函数: 无")
        status_layout.addWidget(self.status_func)
        layout.addWidget(status_frame)

    def _refresh_func_mode(self):
        """刷新函数模式(切换模糊匹配后重新显示)"""
        if self.view_mode == "func":
            self._display_func_mode()

    def _find_matching_functions(self, left_name, right_names):
        """找到匹配的右侧函数名(优先精确匹配)"""
        import difflib
        if left_name in right_names:
            return left_name, "精确匹配"
        if not self.fuzzy_match_cb.isChecked():
            return None, None
        lower_map = {name.lower(): name for name in right_names}
        if left_name.lower() in lower_map:
            return lower_map[left_name.lower()], "忽略大小写"
        left_clean = left_name.strip('_')
        for right_name in right_names:
            right_clean = right_name.strip('_')
            if left_clean == right_clean:
                return right_name, "去前后缀"
            if left_clean.endswith(right_clean) or right_clean.endswith(left_clean):
                return right_name, "包含匹配"
        matches = difflib.get_close_matches(left_name, right_names, n=1, cutoff=0.7)
        if matches:
            return matches[0], f"相似度 {difflib.SequenceMatcher(None, left_name, matches[0]).ratio():.2f}"
        return None, None

    def _count_diff_lines(self, left_text, right_text):
        """统计两个文本的差异行数"""
        import difflib
        differ = difflib.SequenceMatcher(None, left_text.splitlines(), right_text.splitlines())
        added = 0
        removed = 0
        for tag, i1, i2, j1, j2 in differ.get_opcodes():
            if tag == 'replace':
                added += j2 - j1
                removed += i2 - i1
            elif tag == 'delete':
                removed += i2 - i1
            elif tag == 'insert':
                added += j2 - j1
        return added, removed

    def _get_display_funcs(self):
        """获取当前显示的匹配函数列表"""
        result = []
        left_names = list(self.left_funcs.keys())
        right_names = list(self.right_funcs.keys())
        for name in left_names:
            matched, _ = self._find_matching_functions(name, right_names)
            if matched:
                result.append(name)
        return sorted(result)

    def _get_selected_range(self, edit_widget):
        """获取选中区域的行范围"""
        cursor = edit_widget.textCursor()
        start = cursor.selectionStart()
        end = cursor.selectionEnd()
        if start == end:
            return None, None
        doc = edit_widget.document()
        start_block = doc.findBlock(start)
        end_block = doc.findBlock(end)
        return start_block.blockNumber(), end_block.blockNumber()

    def _copy_selected_left_to_right(self):
        """将左侧选中的行覆盖到右侧对应位置"""
        start, end = self._get_selected_range(self.left_edit)
        if start is None:
            show_msg(self, "提示", "请先在左侧选中要覆盖的行",1)
            return
        cursor = self.left_edit.textCursor()
        selected_text = cursor.selectedText()
        reply = QMessageBox.question(
            self, "确认覆盖",
            f"将左侧选中的 {end - start + 1} 行覆盖到右侧对应位置?\n\n右侧原有内容将被替换!",
            QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
        )
        if reply != QMessageBox.StandardButton.Yes:
            return
        right_cursor = self.right_edit.textCursor()
        right_cursor.setPosition(self.right_edit.document().findBlockByNumber(start).position())
        right_cursor.movePosition(QTextCursor.MoveOperation.EndOfBlock, QTextCursor.MoveMode.KeepAnchor)
        for _ in range(end - start):
            right_cursor.movePosition(QTextCursor.MoveOperation.Down, QTextCursor.MoveMode.KeepAnchor)
            right_cursor.movePosition(QTextCursor.MoveOperation.EndOfBlock, QTextCursor.MoveMode.KeepAnchor)
        right_cursor.insertText(selected_text)
        self.right_modified = True
        self.save_right_btn.setEnabled(True)
        self._update_status()
        if self.parent() and hasattr(self.parent(), 'safe_log'):
            self.parent().safe_log(f"✅ 已从左侧覆盖 {end - start + 1} 行到右侧")

    def _copy_selected_right_to_left(self):
        """将右侧选中的行覆盖到左侧对应位置"""
        start, end = self._get_selected_range(self.right_edit)
        if start is None:
            show_msg(self, "提示", "请先在右侧选中要覆盖的行",1)
            return
        cursor = self.right_edit.textCursor()
        selected_text = cursor.selectedText()
        reply = QMessageBox.question(
            self, "确认覆盖",
            f"将右侧选中的 {end - start + 1} 行覆盖到左侧对应位置?\n\n左侧原有内容将被替换!",
            QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
        )
        if reply != QMessageBox.StandardButton.Yes:
            return
        left_cursor = self.left_edit.textCursor()
        left_cursor.setPosition(self.left_edit.document().findBlockByNumber(start).position())
        left_cursor.movePosition(QTextCursor.MoveOperation.EndOfBlock, QTextCursor.MoveMode.KeepAnchor)
        for _ in range(end - start):
            left_cursor.movePosition(QTextCursor.MoveOperation.Down, QTextCursor.MoveMode.KeepAnchor)
            left_cursor.movePosition(QTextCursor.MoveOperation.EndOfBlock, QTextCursor.MoveMode.KeepAnchor)
        left_cursor.insertText(selected_text)
        self.left_modified = True
        self.save_left_btn.setEnabled(True)
        self._update_status()
        if self.parent() and hasattr(self.parent(), 'safe_log'):
            self.parent().safe_log(f"✅ 已从右侧覆盖 {end - start + 1} 行到左侧")

    def _populate_func_list(self):
        self.func_list.clear()
        keyword = self.search_edit.text().strip().lower()
        display_funcs = self._get_display_funcs()
        for name in display_funcs:
            if keyword == "" or keyword in name.lower():
                self.func_list.addItem(name)

    def _on_func_clicked(self, item):
        func_name = item.text()
        right_names = list(self.right_funcs.keys())
        matched, _ = self._find_matching_functions(func_name, right_names)
        if matched:
            self._select_func(func_name, matched)
        else:
            self._select_func(func_name, None)

    def _select_func(self, left_name, right_name=None):
        """选择并显示函数对比"""
        self.current_func = left_name
        if right_name is None:
            right_name = left_name
            matched, _ = self._find_matching_functions(left_name, list(self.right_funcs.keys()))
            if matched:
                right_name = matched
        self.left_edit_title.setText(f"✏️ 源函数: {left_name}")
        self.right_edit_title.setText(f"✏️ 编译函数: {right_name}")
        self.left_edit.clear()
        self.right_edit.clear()
        if left_name in self.left_funcs:
            self.left_edit.setPlainText(self.left_funcs[left_name]['body'])
        if right_name in self.right_funcs:
            self.right_edit.setPlainText(self.right_funcs[right_name]['body'])
        self.left_modified = False
        self.right_modified = False
        self.save_left_btn.setEnabled(False)
        self.save_right_btn.setEnabled(False)
        self._update_status()
        if self.highlight_active:
            self._highlight_diffs()

    def _display_func_mode(self):
        self.view_mode = "func"
        self.func_mode_btn.setStyleSheet("background: #2ecc71; color: white;")
        self.line_mode_btn.setStyleSheet("background: #95a5a6; color: white;")
        self.left_preview.clear()
        self.right_preview.clear()
        left_names = list(self.left_funcs.keys())
        right_names = list(self.right_funcs.keys())
        matched_pairs = []
        used_right = set()
        for name in left_names:
            matched, match_type = self._find_matching_functions(name, right_names)
            if matched and matched not in used_right:
                matched_pairs.append((name, matched, match_type))
                used_right.add(matched)
            elif matched and matched in used_right:
                matched_pairs.append((name, None, "已被其他函数匹配"))
        for name, matched, match_type in matched_pairs:
            if matched:
                left_body = self.left_funcs[name]['body']
                right_body = self.right_funcs[matched]['body']
                left_num = len(left_body.splitlines())
                right_num = len(right_body.splitlines())
                added, removed = self._count_diff_lines(left_body, right_body)
                diff_info = f" [+{added} -{removed}]"
                label = f"函数: {name} ↔ {matched}{diff_info}"
                if match_type != "精确匹配":
                    label += f" [{match_type}]"
                self.left_preview.appendPlainText(f"\n{'='*60}\n{label}\n行数: {left_num}\n{'='*60}")
                self.left_preview.appendPlainText(left_body)
                self.right_preview.appendPlainText(f"\n{'='*60}\n{label}\n行数: {right_num}\n{'='*60}")
                self.right_preview.appendPlainText(right_body)
            else:
                self.left_preview.appendPlainText(f"\n{'='*60}\n函数: {name} (无匹配)\n{'='*60}")
                self.left_preview.appendPlainText(self.left_funcs[name]['body'])
                self.right_preview.appendPlainText(f"\n{'='*60}\n函数: {name} (无匹配)\n{'='*60}")
        self._populate_func_list()
        matched_count = len([p for p in matched_pairs if p[1] is not None])
        total_left = len(left_names)
        self.status_label.setText(f"匹配: {matched_count}/{total_left} 个函数")
        if self.highlight_active:
            self._highlight_diffs()

    def _switch_to_func_mode(self):
        if self.view_mode != "func":
            self._display_func_mode()
            if self.current_func:
                right_names = list(self.right_funcs.keys())
                matched, _ = self._find_matching_functions(self.current_func, right_names)
                if matched:
                    self._select_func(self.current_func, matched)
                else:
                    self._select_func(self.current_func, None)

    def _display_line_mode(self):
        self.view_mode = "line"
        self.func_mode_btn.setStyleSheet("background: #95a5a6; color: white;")
        self.line_mode_btn.setStyleSheet("background: #2ecc71; color: white;")
        self.left_preview.clear()
        self.right_preview.clear()
        differ = difflib.SequenceMatcher(None, self.left_lines, self.right_lines)
        for tag, i1, i2, j1, j2 in differ.get_opcodes():
            if tag == 'equal':
                for i in range(i1, i2):
                    self.left_preview.appendPlainText(self.left_lines.rstrip())
                    self.right_preview.appendPlainText(self.right_lines[j1 + (i-i1)].rstrip())
            elif tag == 'replace':
                for i in range(i1, i2):
                    self.left_preview.appendPlainText("  " + self.left_lines.rstrip())
                for j in range(j1, j2):
                    self.right_preview.appendPlainText("  " + self.right_lines[j].rstrip())
            elif tag == 'delete':
                for i in range(i1, i2):
                    self.left_preview.appendPlainText("- " + self.left_lines.rstrip())
            elif tag == 'insert':
                for j in range(j1, j2):
                    self.right_preview.appendPlainText("+ " + self.right_lines[j].rstrip())
        # 逐行模式下也支持高亮
        if self.highlight_active:
            self._highlight_diffs()

    def _switch_to_func_mode(self):
        if self.view_mode != "func":
            self._display_func_mode()
            if self.current_func:
                self._select_func(self.current_func)

    def _switch_to_line_mode(self):
        if self.view_mode != "line":
            self._display_line_mode()

    def _on_edit(self, side):
        if side == "left":
            self.left_modified = True
            self.save_left_btn.setEnabled(True)
        else:
            self.right_modified = True
            self.save_right_btn.setEnabled(True)
        self._update_status()
        # 编辑后自动清除高亮状态
        if self.highlight_active:
            self.highlight_active = False
            self.highlight_btn.setStyleSheet("background: #e67e22; color: white;")

    def _save_side(self, side):
        if side == "left":
            content = self.left_edit.toPlainText()
            if self.current_func and self.current_func in self.left_funcs:
                old = self.left_funcs[self.current_func]
                new_lines = content.splitlines(True)
                if new_lines and not new_lines[-1].endswith('\n'):
                    new_lines[-1] += '\n'
                self.left_lines[old['start']:old['end']+1] = new_lines
                with open(self.left_file, 'w', encoding='utf-8') as f:
                    f.writelines(self.left_lines)
                self.left_funcs[self.current_func]['body'] = content
                self.left_funcs[self.current_func]['end'] = old['start'] + len(new_lines) - 1
                self.left_modified = False
                self.save_left_btn.setEnabled(False)
                self._refresh_preview()
        else:
            content = self.right_edit.toPlainText()
            if self.current_func and self.current_func in self.right_funcs:
                old = self.right_funcs[self.current_func]
                new_lines = content.splitlines(True)
                if new_lines and not new_lines[-1].endswith('\n'):
                    new_lines[-1] += '\n'
                self.right_lines[old['start']:old['end']+1] = new_lines
                with open(self.right_file, 'w', encoding='utf-8') as f:
                    f.writelines(self.right_lines)
                self.right_funcs[self.current_func]['body'] = content
                self.right_funcs[self.current_func]['end'] = old['start'] + len(new_lines) - 1
                self.right_modified = False
                self.save_right_btn.setEnabled(False)
                self._refresh_preview()
        self._update_status()

    def _revert_side(self, side):
        if side == "left" and self.current_func in self.left_funcs:
            old = self.left_funcs[self.current_func]
            original = ''.join(self.left_original[old['start']:old['end']+1])
            self.left_edit.setPlainText(original.rstrip())
            self.left_funcs[self.current_func]['body'] = original.rstrip()
            self.left_lines[old['start']:old['end']+1] = self.left_original[old['start']:old['end']+1].copy()
            self.left_modified = False
            self.save_left_btn.setEnabled(False)
        elif side == "right" and self.current_func in self.right_funcs:
            old = self.right_funcs[self.current_func]
            original = ''.join(self.right_original[old['start']:old['end']+1])
            self.right_edit.setPlainText(original.rstrip())
            self.right_funcs[self.current_func]['body'] = original.rstrip()
            self.right_lines[old['start']:old['end']+1] = self.right_original[old['start']:old['end']+1].copy()
            self.right_modified = False
            self.save_right_btn.setEnabled(False)
        self._update_status()
        if self.view_mode == "func":
            self._refresh_preview()
        # 还原后重新高亮
        if self.highlight_active:
            self._highlight_diffs()

    def _refresh_preview(self):
        if self.view_mode != "func":
            return
        self.left_preview.clear()
        self.right_preview.clear()
        left_names = list(self.left_funcs.keys())
        right_names = list(self.right_funcs.keys())
        for name in left_names:
            matched, match_type = self._find_matching_functions(name, right_names)
            if matched:
                left_body = self.left_funcs[name]['body']
                right_body = self.right_funcs[matched]['body']
                left_num = len(left_body.splitlines())
                right_num = len(right_body.splitlines())
                label = f"函数: {name} ↔ {matched}"
                if match_type != "精确匹配":
                    label += f" [{match_type}]"
                self.left_preview.appendPlainText(f"\n{'='*60}\n{label}\n行数: {left_num}\n{'='*60}")
                self.left_preview.appendPlainText(left_body)
                self.right_preview.appendPlainText(f"\n{'='*60}\n{label}\n行数: {right_num}\n{'='*60}")
                self.right_preview.appendPlainText(right_body)
            else:
                self.left_preview.appendPlainText(f"\n{'='*60}\n函数: {name} (无匹配)\n{'='*60}")
                self.left_preview.appendPlainText(self.left_funcs[name]['body'])
                self.right_preview.appendPlainText(f"\n{'='*60}\n函数: {name} (无匹配)\n{'='*60}")

    def _on_highlight_clicked(self):
        """高亮差异按钮点击 - 切换高亮状态"""
        if self.highlight_active:
            self._clear_highlights()
            self.highlight_active = False
            self.highlight_btn.setStyleSheet("background: #e67e22; color: white;")
        else:
            self._highlight_diffs()
            self.highlight_active = True
            self.highlight_btn.setStyleSheet("background: #e67e22; color: white; font-weight: bold; border: 2px solid #ff6b00;")
        # 确保清除高亮按钮样式恢复
        self.clear_highlight_btn.setStyleSheet("background: #95a5a6; color: white;")

    def _clear_highlights(self):
        self.left_edit.setExtraSelections([])
        self.right_edit.setExtraSelections([])
        self.status_label.setText("已清除高亮")
        self.highlight_active = False
        self.highlight_btn.setStyleSheet("background: #e67e22; color: white;")
        self.clear_highlight_btn.setStyleSheet("background: #f44336; color: white; font-weight: bold;")
        # 延迟恢复清除按钮样式
        QTimer.singleShot(300, lambda: self.clear_highlight_btn.setStyleSheet("background: #95a5a6; color: white;"))

    def _highlight_diffs(self):
        """高亮差异 - 使用 ExtraSelections"""
        if self.view_mode == "func":
            self._highlight_func_diff()
        else:
            self._highlight_line_diff()

    def _highlight_func_diff(self):
        """函数模式下的高亮 - 只高亮当前选中的函数"""
        if not self.current_func:
            return
        right_name = self.current_func
        matched, _ = self._find_matching_functions(self.current_func, list(self.right_funcs.keys()))
        if matched:
            right_name = matched
        if self.current_func not in self.left_funcs or right_name not in self.right_funcs:
            return
        left_body = self.left_funcs[self.current_func]['body'].splitlines()
        right_body = self.right_funcs[right_name]['body'].splitlines()
        differ = difflib.SequenceMatcher(None, left_body, right_body)
        left_selections = []
        right_selections = []
        for tag, i1, i2, j1, j2 in differ.get_opcodes():
            if tag == 'equal':
                continue
            if tag in ('delete', 'replace'):
                for i in range(i1, i2):
                    if i < self.left_edit.document().blockCount():
                        cursor = self.left_edit.textCursor()
                        cursor.setPosition(self.left_edit.document().findBlockByNumber(i).position())
                        cursor.movePosition(QTextCursor.MoveOperation.EndOfBlock, QTextCursor.MoveMode.KeepAnchor)
                        fmt = QTextCharFormat()
                        fmt.setBackground(QColor("#5a2a2a"))
                        selection = QTextEdit.ExtraSelection()
                        selection.cursor = cursor
                        selection.format = fmt
                        left_selections.append(selection)
            if tag in ('insert', 'replace'):
                for j in range(j1, j2):
                    if j < self.right_edit.document().blockCount():
                        cursor = self.right_edit.textCursor()
                        cursor.setPosition(self.right_edit.document().findBlockByNumber(j).position())
                        cursor.movePosition(QTextCursor.MoveOperation.EndOfBlock, QTextCursor.MoveMode.KeepAnchor)
                        fmt = QTextCharFormat()
                        fmt.setBackground(QColor("#2a5a2a"))
                        selection = QTextEdit.ExtraSelection()
                        selection.cursor = cursor
                        selection.format = fmt
                        right_selections.append(selection)
        self.left_edit.setExtraSelections(left_selections)
        self.right_edit.setExtraSelections(right_selections)
        if left_selections or right_selections:
            self.status_label.setText(f"✅ 差异: 左侧 {len(left_selections)} 处, 右侧 {len(right_selections)} 处")
            self.status_func.setText(f"函数: {self.current_func} | 差异: L{len(left_selections)} R{len(right_selections)}")

    def _highlight_line_diff(self):
        """逐行模式下的高亮 - 高亮全文件差异"""
        differ = difflib.SequenceMatcher(None, self.left_lines, self.right_lines)
        left_selections = []
        right_selections = []
        for tag, i1, i2, j1, j2 in differ.get_opcodes():
            if tag == 'equal':
                continue
            if tag in ('delete', 'replace'):
                for i in range(i1, i2):
                    if i < self.left_edit.document().blockCount():
                        cursor = self.left_edit.textCursor()
                        cursor.setPosition(self.left_edit.document().findBlockByNumber(i).position())
                        cursor.movePosition(QTextCursor.MoveOperation.EndOfBlock, QTextCursor.MoveMode.KeepAnchor)
                        fmt = QTextCharFormat()
                        fmt.setBackground(QColor("#5a2a2a"))
                        selection = QTextEdit.ExtraSelection()
                        selection.cursor = cursor
                        selection.format = fmt
                        left_selections.append(selection)
            if tag in ('insert', 'replace'):
                for j in range(j1, j2):
                    if j < self.right_edit.document().blockCount():
                        cursor = self.right_edit.textCursor()
                        cursor.setPosition(self.right_edit.document().findBlockByNumber(j).position())
                        cursor.movePosition(QTextCursor.MoveOperation.EndOfBlock, QTextCursor.MoveMode.KeepAnchor)
                        fmt = QTextCharFormat()
                        fmt.setBackground(QColor("#2a5a2a"))
                        selection = QTextEdit.ExtraSelection()
                        selection.cursor = cursor
                        selection.format = fmt
                        right_selections.append(selection)
        self.left_edit.setExtraSelections(left_selections)
        self.right_edit.setExtraSelections(right_selections)
        if left_selections or right_selections:
            self.status_label.setText(f"✅ 差异: 左侧 {len(left_selections)} 处, 右侧 {len(right_selections)} 处")

    def _toggle_sync(self):
        self.sync_mode = not self.sync_mode
        if self.sync_mode:
            self._bind_sync()
            self.sync_btn.setText("🔗 滚动 (开)")
            self.sync_btn.setStyleSheet("background: #2ecc71; color: white;")
        else:
            self._unbind_sync()
            self.sync_btn.setText("🔗 滚动 (关)")
            self.sync_btn.setStyleSheet("background: #3498db; color: white;")

    def _bind_sync(self):
        all_texts = [self.left_preview, self.right_preview, self.left_edit, self.right_edit]
        for text in all_texts:
            text.wheelEvent = lambda e, t=text: self._on_wheel(e, t)

    def _unbind_sync(self):
        all_texts = [self.left_preview, self.right_preview, self.left_edit, self.right_edit]
        for text in all_texts:
            text.wheelEvent = None

    def _on_wheel(self, event, source):
        if not self.sync_mode:
            return
        delta = event.angleDelta().y()
        if delta == 0:
            return
        current = source.verticalScrollBar().value()
        new_pos = current - delta // 4
        all_texts = [self.left_preview, self.right_preview, self.left_edit, self.right_edit]
        for text in all_texts:
            text.verticalScrollBar().setValue(new_pos)
        event.accept()

    def _reload_files(self):
        self._load_files()
        self._extract_functions()
        if self.view_mode == "func":
            self._display_func_mode()
            if self.current_func:
                matched, _ = self._find_matching_functions(self.current_func, list(self.right_funcs.keys()))
                if matched:
                    self._select_func(self.current_func, matched)
                else:
                    self._select_func(self.current_func, None)
        else:
            self._display_line_mode()
        self.left_modified = False
        self.right_modified = False
        self._update_status()

    def _select_files(self):
        left = QFileDialog.getOpenFileName(self, "选择源文件", "", "Python文件 (*.py)")[0]
        if not left:
            return
        right = QFileDialog.getOpenFileName(self, "选择编译文件", "", "Python文件 (*.py)")[0]
        if not right:
            return
        self.left_file = left
        self.right_file = right
        self.setWindowTitle(f"代码对比 - {os.path.basename(left)} ↔ {os.path.basename(right)}")
        self._reload_files()

    def _export_report(self):
        path = QFileDialog.getSaveFileName(self, "导出报告", "", "文本文件 (*.txt)")[0]
        if not path:
            return
        report = ["=" * 60]
        report.append("代码对比报告")
        report.append(f"源文件: {os.path.basename(self.left_file)}")
        report.append(f"编译文件: {os.path.basename(self.right_file)}")
        report.append(f"时间: {datetime.now()}")
        report.append("=" * 60)
        left_names = list(self.left_funcs.keys())
        right_names = list(self.right_funcs.keys())
        matched_count = 0
        report.append("")
        report.append("📊 函数匹配统计:")
        report.append("-" * 40)
        for name in left_names:
            matched, match_type = self._find_matching_functions(name, right_names)
            if matched:
                matched_count += 1
                left_body = self.left_funcs[name]['body']
                right_body = self.right_funcs[matched]['body']
                ratio = difflib.SequenceMatcher(None, left_body, right_body).ratio() * 100
                added, removed = self._count_diff_lines(left_body, right_body)
                status = f"相似度: {ratio:.1f}%  [+{added} -{removed}]"
                if match_type != "精确匹配":
                    status += f" [{match_type}]"
                report.append(f"  {name} ↔ {matched}")
                report.append(f"    {status}")
        report.append("")
        report.append(f"匹配率: {matched_count}/{len(left_names)} ({matched_count/len(left_names)*100:.1f}%)" if left_names else "0/0")
        with open(path, 'w', encoding='utf-8') as f:
            f.write('\n'.join(report))
        self.status_label.setText(f"✅ 报告已导出: {os.path.basename(path)}")

    def _filter_func_list(self):
        self._populate_func_list()

    def _update_status(self):
        self.status_label.setText(f"函数: {self.current_func or '无'} | 左侧修改: {'是' if self.left_modified else '否'} | 右侧修改: {'是' if self.right_modified else '否'}")
        self.status_func.setText(f"函数: {self.current_func or '无'}")

class SystemMonitorThread(QThread):
    """系统资源监控线程 - 低频更新,温度显示一位小数"""
    status_updated = pyqtSignal(float, float, float, float, str)
    def __init__(self):
        super().__init__()
        self._is_running = True
        self._interval = 3000
        self._lhm_computer = None
        self._init_lhm()

    def _init_lhm(self):
        """初始化 LibreHardwareMonitor(Windows 最佳方案)"""
        if sys.platform != 'win32':
            return
        try:
            import clr
            dll_paths = [
                'LibreHardwareMonitorLib.dll',
                './LibreHardwareMonitorLib.dll',
                '../LibreHardwareMonitorLib.dll',
                os.path.join(os.path.dirname(__file__), 'LibreHardwareMonitorLib.dll'),
            ]
            dll_loaded = False
            for dll_path in dll_paths:
                if os.path.exists(dll_path):
                    clr.AddReference(dll_path)
                    dll_loaded = True
                    break
            if not dll_loaded:
                return
            from LibreHardwareMonitor.Hardware import Computer, HardwareType, SensorType
            self._lhm_computer = Computer()
            self._lhm_computer.IsCpuEnabled = True
            self._lhm_computer.IsGpuEnabled = True
            self._lhm_computer.IsMotherboardEnabled = True
            self._lhm_computer.Open()
        except Exception:
            self._lhm_computer = None

    def _get_temp_lhm(self):
        """通过 LibreHardwareMonitor 获取温度(返回 float)"""
        if not self._lhm_computer:
            return None
        try:
            from LibreHardwareMonitor.Hardware import HardwareType, SensorType
            for hardware in self._lhm_computer.Hardware:
                if hardware.HardwareType == HardwareType.Cpu:
                    hardware.Update()
                    for sensor in hardware.Sensors:
                        if (sensor.SensorType == SensorType.Temperature and
                            'package' in sensor.Name.lower()):
                            val = sensor.Value
                            if val is not None and -10 < val < 120:
                                return float(val)
                    for sensor in hardware.Sensors:
                        if sensor.SensorType == SensorType.Temperature:
                            val = sensor.Value
                            if val is not None and -10 < val < 120:
                                return float(val)
        except Exception:
            pass
        return None

    def _get_temp_psutil(self):
        """通过 psutil 获取温度(Linux/macOS 有效,返回 float)"""
        try:
            import psutil
            temps = psutil.sensors_temperatures()
            if not temps:
                return None
            priority_keys = ['coretemp', 'k10temp', 'zenpower', 'cpu_thermal',
                             'acpitz', 'pch_skylake']
            for key in priority_keys:
                if key in temps:
                    entries = temps[key]
                    if entries:
                        for entry in entries:
                            if entry.current and 10 < entry.current < 120:
                                return float(entry.current)
            for name, entries in temps.items():
                if entries:
                    for entry in entries:
                        if entry.current and 10 < entry.current < 120:
                            return float(entry.current)
        except Exception:
            pass
        return None

    def _get_temp_wmic(self):
        """通过 wmic 命令获取温度(Windows 备用,返回 float)"""
        if sys.platform != 'win32':
            return None
        try:
            import subprocess
            result = subprocess.run(
                ['wmic', r'/namespace:\\root\wmi',
                 'PATH', 'MSAcpi_ThermalZoneTemperature',
                 'GET', 'CurrentTemperature'],
                capture_output=True, text=True, timeout=5,
                creationflags=subprocess.CREATE_NO_WINDOW
            )
            lines = [l.strip() for l in result.stdout.split('\n')
                     if l.strip().replace('.', '').isdigit()]
            if lines:
                temp_k = float(lines[0]) / 10.0
                temp_c = temp_k - 273.15
                if -10 < temp_c < 120:
                    return float(temp_c)
        except Exception:
            pass
        return None

    def _get_temperature(self):
        """获取温度:按优先级尝试多种方案"""
        temp = self._get_temp_lhm()
        if temp is not None:
            return f"{temp:.1f}°C"
        temp = self._get_temp_psutil()
        if temp is not None:
            return f"{temp:.1f}°C"
        temp = self._get_temp_wmic()
        if temp is not None:
            return f"{temp:.1f}°C"
        return ""

    def _get_resource_color(self, percent, is_temp=False):
        """获取资源颜色(三色:绿→橙→红)"""
        if is_temp:
            if percent > 80:
                return "#F44336"
            elif percent > 65:
                return "#FF9800"
            else:
                return "#4CAF50"
        else:
            if percent > 80:
                return "#F44336"
            elif percent > 60:
                return "#FF9800"
            else:
                return "#4CAF50"

    def run(self):
        try:
            import psutil
        except ImportError:
            return
        while self._is_running:
            try:
                cpu_percent = psutil.cpu_percent(interval=1)
                memory = psutil.virtual_memory()
                temp_str = self._get_temperature()
                self.status_updated.emit(
                    cpu_percent,
                    memory.percent,
                    memory.used / (1024 ** 3),
                    memory.total / (1024 ** 3),
                    temp_str
                )
            except Exception:
                pass
            if self._is_running:
                self.msleep(self._interval)

    def stop(self):
        self._is_running = False
        self.wait(1000)
        if self._lhm_computer:
            try:
                self._lhm_computer.Close()
            except Exception:
                pass

class PackThread(QThread):
    progress_signal = pyqtSignal(int)
    log_signal = pyqtSignal(str)
    finished_signal = pyqtSignal(bool, str)
    def __init__(self, config):
        super().__init__()
        self.config = config
        self._is_running = True
        self.process = None
        self._mock_progress = 0
        self._mock_timer = None
        self._real_progress_received = False
        self._output_buffer = []  

    def run(self):
        process = None
        try:
            self.log_signal.emit("🚀 开始打包...")
            cmd = self._build_command()
            process = self._popen_hidden(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                text=True,
                encoding="utf-8",
                errors="replace",
                startupinfo=get_startupinfo(),
                bufsize=1,
                universal_newlines=True
            )
            self.process = process
            self._real_progress_received = False
            self._mock_progress = 0
            self._start_mock_progress()
            import select
            import sys
            while self._is_running and process.poll() is None:
                try:
                    if sys.platform == 'win32':
                        # Windows使用timeout轮询
                        import time
                        time.sleep(0.1)
                        line = process.stdout.readline()
                        if line:
                            self._process_output_line(line)
                    else:
                        import select
                        if select.select([process.stdout], [], [], 0.1)[0]:
                            line = process.stdout.readline()
                            if line:
                                self._process_output_line(line)
                except Exception as e:
                    if self._is_running:
                        self.log_signal.emit(f"读取输出异常: {e}")
                    break
                if not self._is_running:
                    break
            if self._is_running:
                for line in process.stdout:
                    if not self._is_running:
                        break
                    self._process_output_line(line)
            if process.poll() is None:
                process.wait(timeout=30)
            returncode = process.returncode if process.poll() is not None else -1
            self._stop_mock_progress()
            if returncode == 0:
                self.progress_signal.emit(100)
                self.finished_signal.emit(True, "打包完成!")
            else:
                self.finished_signal.emit(False, f"返回码: {returncode}")
        except subprocess.TimeoutExpired:
            self._stop_mock_progress()
            self.finished_signal.emit(False, "打包超时")
        except Exception as e:
            self._stop_mock_progress()
            self.finished_signal.emit(False, str(e))
        finally:
            self._cleanup_process(process)

    def _process_output_line(self, line):
        """处理输出行"""
        if not line:
            return
        line = line.rstrip()
        if line:
            self.log_signal.emit(line)
            p = self._parse_progress(line)
            if p is not None:
                if not self._real_progress_received:
                    self._real_progress_received = True
                    self._stop_mock_progress()
                self.progress_signal.emit(p)

    def _start_mock_progress(self):
        """启动模拟进度"""
        self._mock_progress = 0
        self._real_progress_received = False
        self._mock_timer = QTimer()
        self._mock_timer.timeout.connect(self._update_mock_progress)
        self._mock_timer.start(2000)  

    def _update_mock_progress(self):
        """更新模拟进度(更慢的增长)"""
        if self._real_progress_received:
            self._stop_mock_progress()
            return
        if self._mock_progress < 5:
            self._mock_progress += 1
            self.progress_signal.emit(self._mock_progress)

    def _stop_mock_progress(self):
        """停止模拟进度"""
        if self._mock_timer:
            self._mock_timer.stop()
            self._mock_timer.deleteLater()
            self._mock_timer = None

    def _cleanup_process(self, process):
        """彻底清理进程"""
        if process:
            try:
                if process.poll() is None:
                    process.terminate()
                    try:
                        process.wait(timeout=3)
                    except subprocess.TimeoutExpired:
                        process.kill()
                        process.wait(timeout=2)
                if process.stdout:
                    process.stdout.close()
                if process.stderr:
                    process.stderr.close()
            except Exception as e:
                pass
            finally:
                self.process = None
                import gc
                gc.collect()

    def stop(self):
        """停止打包(改进版)"""
        self._is_running = False
        self._stop_mock_progress()
        if self.process:
            try:
                if sys.platform == 'win32':
                    self.process.terminate()
                    try:
                        self.process.wait(timeout=3)
                    except subprocess.TimeoutExpired:
                        self.process.kill()
                else:
                    self.process.terminate()
                    try:
                        self.process.wait(timeout=3)
                    except subprocess.TimeoutExpired:
                        self.process.kill()
            except Exception as e:
                pass
            finally:
                self.process = None
class AboutDialog(QDialog):
    """关于对话框"""
    def __init__(self, parent=None):
        super().__init__(parent)
        self.parent = parent
        self.setWindowTitle("关于 - Python代码跨平台打包工具")
        # 获取屏幕大小并设置为80%
        screen = QApplication.primaryScreen()
        screen_geometry = screen.availableGeometry()
        screen_width = screen_geometry.width()
        screen_height = screen_geometry.height()
        window_width = int(screen_width * 0.8)
        window_height = int(screen_height * 0.8)
        self.setMinimumSize(window_width, window_height)
        self.setModal(True)
        self.loader = None
        self.setStyleSheet("""
            QDialog {
                background-color: #f5f5f5;
            }
            QTextEdit {
                background-color: white;
                border: 1px solid #d0d0d0;
                border-radius: 4px;
                padding: 8px;
            }
            QScrollBar:vertical {
                border: none;
                background: #f0f0f0;
                width: 12px;
                border-radius: 6px;
            }
            QScrollBar::handle:vertical {
                background: #c0c0c0;
                border-radius: 6px;
                min-height: 20px;
            }
            QScrollBar::handle:vertical:hover {
                background: #a0a0a0;
            }
            QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
                height: 0px;
            }
            QPushButton {
                background-color: #4CAF50;
                color: white;
                border: none;
                padding: 8px 16px;
                border-radius: 4px;
                font-size: 12px;
            }
            QPushButton:hover {
                background-color: #45a049;
            }
            QPushButton#closeBtn {
                background-color: #f44336;
            }
            QPushButton#closeBtn:hover {
                background-color: #da190b;
            }
            QPushButton#editBtn {
                background-color: #ff9800;
            }
            QPushButton#editBtn:hover {
                background-color: #e68900;
            }
            QLabel#titleLabel {
                color: #2196F3;
            }
            QSplitter::handle {
                background-color: #d0d0d0;
                width: 3px;
            }
            QSplitter::handle:hover {
                background-color: #2196F3;
            }
        """)
        self.init_ui()
        self.load_content_async()  
        self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)

    def init_ui(self):
        """初始化界面"""
        main_layout = QVBoxLayout(self)
        main_layout.setContentsMargins(25, 25, 25, 25)
        main_layout.setSpacing(15)
        # 顶部标题区域
        title_widget = QWidget()
        title_layout = QVBoxLayout(title_widget)
        title_layout.setSpacing(8)
        # 主标题
        title_label = QLabel("🐍 Python 代码跨平台打包工具")
        title_label.setObjectName("titleLabel")
        title_label.setFont(QFont("Microsoft YaHei", 22, QFont.Weight.Bold))
        title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        title_layout.addWidget(title_label)
        # 版本信息
        version_label = QLabel(f"版本: {VERSION} |  编译日期: {BUILD_DATE}  |  作者: {AUTHOR}")
        version_label.setFont(QFont("Microsoft YaHei", 10))
        version_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        version_label.setStyleSheet("color: #666;")
        title_layout.addWidget(version_label)
        # 开发环境提示(条件显示)
        if self.should_show_dev_mode():
            dev_label = QLabel("💡 开发模式:编辑 CHANGELOG.txt 和 TUTORIAL.txt 可更新内容")
            dev_label.setFont(QFont("Microsoft YaHei", 8))
            dev_label.setStyleSheet("color: #ff6b00; background-color: #fff3e0; padding: 4px 10px; border-radius: 4px;")
            dev_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
            title_layout.addWidget(dev_label)
        main_layout.addWidget(title_widget)
        # 分割线
        separator = QFrame()
        separator.setFrameShape(QFrame.Shape.HLine)
        separator.setFrameShadow(QFrame.Shadow.Sunken)
        separator.setStyleSheet("background-color: #e0e0e0; max-height: 2px;")
        main_layout.addWidget(separator)
        # ========== 左右分栏(扩大中间区域) ==========
        splitter = QSplitter(Qt.Orientation.Horizontal)
        splitter.setHandleWidth(5)
        # ----- 左侧:更新日志 -----
        left_widget = QWidget()
        left_layout = QVBoxLayout(left_widget)
        left_layout.setContentsMargins(0, 0, 10, 0)
        left_layout.setSpacing(8)
        # 左侧标题栏
        left_header = QWidget()
        left_header_layout = QHBoxLayout(left_header)
        left_header_layout.setContentsMargins(0, 0, 0, 0)
        left_title = QLabel("📝 更新日志")
        left_title.setFont(QFont("Microsoft YaHei", 13, QFont.Weight.Bold))
        left_title.setStyleSheet("color: #2196F3;")
        left_header_layout.addWidget(left_title)
        left_header_layout.addStretch()
        if self.should_show_dev_mode():
            refresh_btn = QPushButton("🔄")
            refresh_btn.setFixedSize(30, 30)
            refresh_btn.setToolTip("重新加载内容")
            refresh_btn.setStyleSheet("""
                QPushButton {
                    background-color: transparent;
                    color: #666;
                    border: 1px solid #d0d0d0;
                    border-radius: 15px;
                    font-size: 14px;
                }
                QPushButton:hover {
                    background-color: #e3f2fd;
                    border-color: #2196F3;
                }
            """)
            refresh_btn.clicked.connect(self.refresh_content)
            left_header_layout.addWidget(refresh_btn)
            left_layout.addWidget(left_header)
        # 左侧文本编辑框
        self.changelog_text = QTextEdit()
        self.changelog_text.setFont(QFont("Consolas", 10))
        self.changelog_text.setReadOnly(True)
        self.changelog_text.setFrameShape(QFrame.Shape.NoFrame)
        self.changelog_text.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
        self.changelog_text.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
        self.changelog_text.setStyleSheet("""
            QTextEdit {
                background-color: white;
                border: 1px solid #d0d0d0;
                border-radius: 4px;
                padding: 10px;
                line-height: 1.6;
            }
        """)
        # 显示加载中
        self.changelog_text.setText("⏳ 正在加载更新日志...")
        left_layout.addWidget(self.changelog_text)
        splitter.addWidget(left_widget)
        # ----- 右侧:使用教程 -----
        right_widget = QWidget()
        right_layout = QVBoxLayout(right_widget)
        right_layout.setContentsMargins(10, 0, 0, 0)
        right_layout.setSpacing(8)
        right_title = QLabel("📖 使用教程")
        right_title.setFont(QFont("Microsoft YaHei", 13, QFont.Weight.Bold))
        right_title.setStyleSheet("color: #4CAF50;")
        right_layout.addWidget(right_title)
        # 右侧文本编辑框
        self.tutorial_text = QTextEdit()
        self.tutorial_text.setFont(QFont("Microsoft YaHei", 10))
        self.tutorial_text.setReadOnly(True)
        self.tutorial_text.setFrameShape(QFrame.Shape.NoFrame)
        self.tutorial_text.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
        self.tutorial_text.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
        self.tutorial_text.setStyleSheet("""
            QTextEdit {
                background-color: white;
                border: 1px solid #d0d0d0;
                border-radius: 4px;
                padding: 10px;
                line-height: 1.8;
            }
        """)
        # 显示加载中
        self.tutorial_text.setText("⏳ 正在加载使用教程...")
        right_layout.addWidget(self.tutorial_text)
        splitter.addWidget(right_widget)
        # 设置左右比例(各占一半,让内容区更大)
        splitter.setSizes([600, 600])
        main_layout.addWidget(splitter, 1)  # stretch=1 让splitter占据更多空间
        # ========== 底部按钮区域 ==========
        btn_widget = QWidget()
        btn_widget.setStyleSheet("""
            QWidget {
                background-color: white;
                border-top: 1px solid #e0e0e0;
                padding: 10px 0;
            }
        """)
        btn_layout = QHBoxLayout(btn_widget)
        btn_layout.setSpacing(12)
        btn_layout.setContentsMargins(5, 5, 5, 5)
        # 左侧按钮组
        left_btn_layout = QHBoxLayout()
        left_btn_layout.setSpacing(10)
        # 项目主页
        home_btn = QPushButton("🌐 项目主页")
        home_btn.setFixedWidth(130)
        home_btn.setStyleSheet("""
            QPushButton {
                background-color: #2196F3;
                color: white;
                border: none;
                padding: 10px 20px;
                border-radius: 5px;
                font-size: 12px;
                font-weight: bold;
            }
            QPushButton:hover {
                background-color: #1976D2;
            }
        """)
        home_btn.clicked.connect(lambda: QDesktopServices.openUrl(QUrl("https://github.com/wcj6376")))
        left_btn_layout.addWidget(home_btn)
        feedback_btn = QPushButton("📧 反馈问题")
        feedback_btn.setFixedWidth(130)
        feedback_btn.setStyleSheet("""
            QPushButton {
                background-color: #4CAF50;
                color: white;
                border: none;
                padding: 10px 20px;
                border-radius: 5px;
                font-size: 12px;
                font-weight: bold;
            }
            QPushButton:hover {
                background-color: #45a049;
            }
        """)
        feedback_btn.clicked.connect(self.send_email)
        left_btn_layout.addWidget(feedback_btn)
        # 编辑内容按钮 - 始终显示(源码运行时)
        if self.should_show_dev_mode():
            edit_btn = QPushButton("📁 编辑内容")
            edit_btn.setObjectName("editBtn")
            edit_btn.setFixedWidth(130)
            edit_btn.setStyleSheet("""
                QPushButton {
                    background-color: #FF9800;
                    color: white;
                    border: none;
                    padding: 10px 20px;
                    border-radius: 5px;
                    font-size: 12px;
                    font-weight: bold;
                }
                QPushButton:hover {
                    background-color: #F57C00;
                }
            """)
            edit_btn.clicked.connect(self.open_about_files)
            left_btn_layout.addWidget(edit_btn)
        btn_layout.addLayout(left_btn_layout)
        btn_layout.addStretch()
        # 右侧关闭按钮
        close_btn = QPushButton("✕ 关闭")
        close_btn.setObjectName("closeBtn")
        close_btn.setFixedWidth(130)
        close_btn.setStyleSheet("""
            QPushButton {
                background-color: #f44336;
                color: white;
                border: none;
                padding: 10px 20px;
                border-radius: 5px;
                font-size: 12px;
                font-weight: bold;
            }
            QPushButton:hover {
                background-color: #D32F2F;
            }
        """)
        close_btn.clicked.connect(self.accept)
        btn_layout.addWidget(close_btn)
        main_layout.addWidget(btn_widget)

    def should_show_dev_mode(self):
        """判断是否显示开发模式 - 源码运行时返回True"""
        import sys
        import os
        if hasattr(sys, '_MEIPASS'):
            return False
        if '__compiled__' in globals():
            return False
        if getattr(sys, 'frozen', False):
            return False
        exe_path = sys.argv[0]
        if exe_path.endswith('.exe') and not exe_path.endswith('.py.exe'):
            py_file = exe_path[:-4] + '.py'
            if not os.path.exists(py_file):
                return False
        return True

    def load_content_async(self):
        """异步加载内容"""
        if self.loader and self.loader.isRunning():
            return
        self.loader = ContentLoader()
        self.loader.finished.connect(self.on_content_loaded)
        self.loader.start()

    def on_content_loaded(self, changelog, tutorial):
        """内容加载完成"""
        self.changelog_text.setText(changelog)
        self.tutorial_text.setText(tutorial)
        self.changelog_text.moveCursor(self.changelog_text.textCursor().MoveOperation.Start)
        self.tutorial_text.moveCursor(self.tutorial_text.textCursor().MoveOperation.Start)

    def refresh_content(self):
        """刷新内容"""
        self.changelog_text.setText("⏳ 正在重新加载更新日志...")
        self.tutorial_text.setText("⏳ 正在重新加载使用教程...")
        self.load_content_async()

    def send_email(self):
        """发送邮件"""
        try:
            webbrowser.open("mailto:your-email@example.com?subject=Python打包工具反馈")
        except Exception as e:
            QMessageBox.warning(self, "错误", f"无法打开邮件客户端: {str(e)}")

    def open_about_files(self):
        """打开编辑内容文件"""
        try:
            if not os.path.exists("CHANGELOG.txt"):
                with open("CHANGELOG.txt", "w", encoding="utf-8") as f:
                    f.write(ContentLoader.get_default_changelog(ContentLoader))
                show_msg(self, "提示", "已创建 CHANGELOG.txt 示例文件",1)
            if not os.path.exists("TUTORIAL.txt"):
                with open("TUTORIAL.txt", "w", encoding="utf-8") as f:
                    f.write(ContentLoader.get_default_tutorial(ContentLoader))
                show_msg(self, "提示", "已创建 TUTORIAL.txt 示例文件",1)
            # 打开文件
            if sys_platform.system() == 'Windows':
                if os.path.exists("CHANGELOG.txt"):
                    os.startfile("CHANGELOG.txt")
                if os.path.exists("TUTORIAL.txt"):
                    os.startfile("TUTORIAL.txt")
            else:
                import subprocess
                if os.path.exists("CHANGELOG.txt"):
                    self._popen_hidden(["xdg-open", "CHANGELOG.txt"])
                if os.path.exists("TUTORIAL.txt"):
                    self._popen_hidden(["xdg-open", "TUTORIAL.txt"])
        except Exception as e:
            QMessageBox.warning(self, "错误", f"无法打开文件: {str(e)}")

    def closeEvent(self, event):
        """关闭事件 - 清理资源"""
        if hasattr(self, 'loader') and self.loader is not None:
            if self.loader.isRunning():
                self.loader.quit()
                self.loader.wait()
            self.loader = None
        event.accept()

def get_startupinfo():
    """获取启动信息,隐藏控制台窗口"""
    if sys.platform == 'win32':
        si = subprocess.STARTUPINFO()
        si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
        si.wShowWindow = subprocess.SW_HIDE
        return si
    return None

def get_exe_directory():
    if getattr(sys, 'frozen', False) or os.environ.get('NUITKA_ONEFILE_PARENT') is not None:
        return os.path.dirname(os.path.abspath(sys.argv[0]))
    return os.path.dirname(os.path.abspath(__file__))
APP_BASE_PATH = get_exe_directory()

def _get_real_python():
    """获取真实的Python解释器路径(绝不返回exe自身)"""
    import os, subprocess, sys, shutil

    # 源码模式:直接返回当前Python
    if not getattr(sys, 'frozen', False):
        return sys.executable

    def is_same_file(path1, path2):
        try:
            return os.path.samefile(path1, path2)
        except Exception:
            return os.path.abspath(path1) == os.path.abspath(path2)

    def is_valid_python(path):
        """检查是否是有效的Python且不是当前exe"""
        if not path or not os.path.exists(path):
            return False
        if is_same_file(path, sys.executable):
            return False
        try:
            result = subprocess.run(
                [path, '--version'],
                capture_output=True, text=True, timeout=3,
                startupinfo=get_startupinfo()
            )
            return result.returncode == 0 and ('Python' in (result.stdout + result.stderr))
        except Exception:
            return False

    def run_hidden(args, **kw):
        """隐藏窗口运行命令"""
        if sys.platform == 'win32':
            kw.setdefault('startupinfo', get_startupinfo())
            kw.setdefault('creationflags', subprocess.CREATE_NO_WINDOW)
        return subprocess.run(args, capture_output=True, text=True, timeout=5, **kw)

    # 1. 尝试 py 启动器 (Windows)
    if sys.platform == 'win32':
        try:
            r = run_hidden(['py', '-c', 'import sys; print(sys.executable)'])
            if r.returncode == 0 and r.stdout.strip():
                py_path = r.stdout.strip()
                if is_valid_python(py_path):
                    return py_path
        except Exception:
            pass

    # 2. 尝试 python3 / python 命令
    for py_name in ['python3', 'python']:
        try:
            r = run_hidden([py_name, '-c', 'import sys; print(sys.executable)'])
            if r.returncode == 0 and r.stdout.strip():
                py_path = r.stdout.strip()
                if is_valid_python(py_path):
                    return py_path
        except Exception:
            pass

    # 3. 常见安装路径
    if sys.platform == 'win32':
        username = os.environ.get('USERNAME', '')
        default_paths = [
            r'C:\Python314\python.exe', r'C:\Python313\python.exe',
            r'C:\Python312\python.exe', r'C:\Python311\python.exe',
            r'C:\Python310\python.exe', r'C:\Python39\python.exe',
            rf'C:\Users\{username}\AppData\Local\Programs\Python\Python314\python.exe',
            rf'C:\Users\{username}\AppData\Local\Programs\Python\Python313\python.exe',
            rf'C:\Users\{username}\AppData\Local\Programs\Python\Python312\python.exe',
            rf'C:\Users\{username}\AppData\Local\Programs\Python\Python311\python.exe',
            rf'C:\Users\{username}\AppData\Local\Programs\Python\Python310\python.exe',
        ]
    elif sys.platform == 'darwin':
        default_paths = [
            '/usr/local/bin/python3', '/usr/bin/python3', '/opt/homebrew/bin/python3',
        ]
    else:
        default_paths = [
            '/usr/bin/python3', '/usr/local/bin/python3', '/usr/bin/python',
        ]

    for p in default_paths:
        if is_valid_python(p):
            return p

    # 4. 在 MEIPASS 附近搜索 (PyInstaller打包后)
    if hasattr(sys, '_MEIPASS'):
        meipass = sys._MEIPASS
        search_dir = os.path.dirname(meipass)
        try:
            for root, dirs, files in os.walk(search_dir):
                for f in files:
                    if f.lower() in ('python.exe', 'python3.exe', 'python'):
                        p = os.path.join(root, f)
                        if is_valid_python(p):
                            return p
                if len(root) > len(search_dir) + 100:
                    break
        except Exception:
            pass

    # 5. PATH 中搜索
    for cmd in ['python3', 'python', 'py']:
        p = shutil.which(cmd)
        if p and is_valid_python(p):
            return p

    return None

def get_cache_dir():
    """获取可写的缓存目录"""
    exe_dir = get_exe_directory()
    if os.access(exe_dir, os.W_OK):
        return exe_dir
    import tempfile
    fallback = os.path.join(tempfile.gettempdir(), "PyPackTool")
    os.makedirs(fallback, exist_ok=True)
    return fallback
CACHE_FILE = os.path.join(get_exe_directory(), "global_cache.json")
# ===== 全局内存缓存 =====
_memory_cache = None

def load_cache():
    """加载缓存 - 优先内存"""
    global _memory_cache
    if _memory_cache is not None:
        return _memory_cache
    try:
        if os.path.exists(CACHE_FILE):
            with open(CACHE_FILE, 'r', encoding='utf-8') as f:
                _memory_cache = json.load(f)
                return _memory_cache
    except:
        pass
    _memory_cache = {}
    return _memory_cache

def save_cache(cache):
    """保存缓存到文件并更新内存"""
    global _memory_cache
    _memory_cache = cache
    try:
        with open(CACHE_FILE, 'w', encoding='utf-8') as f:
            json.dump(cache, f, ensure_ascii=False, indent=2)
    except:
        pass

def now_str():
    """返回当前时间的字符串格式 HH:MM:SS.mmm"""
    import datetime
    now = datetime.datetime.now()
    return now.strftime("%H:%M:%S") + f".{now.microsecond//1000:03d}"

class StripedProgressBar(QProgressBar):
    """彩色条纹进度条 - 色彩丰富"""
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setMinimumHeight(20)
        self.setStyleSheet("""
            QProgressBar {
                border: none;
                border-radius: 10px;
                background-color: #f0f2f5;
                text-align: center;
                color: #1a1a2e;
                font-weight: bold;
                font-size: 10px;
            }
            QProgressBar::chunk {
                border-radius: 10px;
                background: qlineargradient(
                    x1: 0, y1: 0, x2: 1, y2: 0,
                    stop: 0 #ff6b6b,
                    stop: 0.2 #feca57,
                    stop: 0.4 #48dbfb,
                    stop: 0.6 #1dd1a1,
                    stop: 0.8 #5f27cd,
                    stop: 1 #ff6b6b
                );
            }
        """)
        self.setValue(0)

    def setValue(self, value):
        super().setValue(value)
        if value < 30:
            self.setFormat(f"🔍 分析中... {value}%")
        elif value < 60:
            self.setFormat(f"⚙️ 编译中... {value}%")
        elif value < 90:
            self.setFormat(f"🚀 打包中... {value}%")
        else:
            self.setFormat(f"✅ 完成! {value}%")

class EmojiProgressBar(QProgressBar):
    """表情动画进度条 - 简洁风格"""
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setMinimumHeight(24)
        self.setStyleSheet("""
            QProgressBar {
                border: 2px solid #0984e3;
                border-radius: 12px;
                background-color: #dfe6e9;
                text-align: center;
                color: #2d3436;
                font-weight: bold;
                font-size: 11px;
            }
            QProgressBar::chunk {
                border-radius: 10px;
                background: qlineargradient(
                    x1: 0, y1: 0, x2: 1, y2: 0,
                    stop: 0 #00cec9,
                    stop: 1 #0984e3
                );
            }
        """)
        self.setValue(0)

    def setValue(self, value):
        super().setValue(value)
        if value < 20:
            self.setFormat(f"🌙 初始化... {value}%")
        elif value < 40:
            self.setFormat(f"📦 收集依赖... {value}%")
        elif value < 60:
            self.setFormat(f"⚡ 编译中... {value}%")
        elif value < 80:
            self.setFormat(f"🎯 打包中... {value}%")
        else:
            self.setFormat(f"✨ 完成! {value}%")

class WaveProgressBar(QWidget):
    """波浪进度条"""
    def __init__(self, parent=None):
        super().__init__(parent)
        self._value = 0
        self.setFixedHeight(60)
        self.offset = 0
        self.timer = QTimer()
        self.timer.timeout.connect(self._animate)
        self.timer.start(50)
        self._format = ""
        self.setValue(0)

    def setValue(self, value):
        self._value = value

    def value(self):
        return self._value

    def setMinimum(self, value):
        """兼容 QProgressBar 接口"""
        pass

    def setMaximum(self, value):
        pass

    def setFormat(self, format_str):
        self._format = format_str
        if '%' in format_str:
            self._format = format_str.replace('%', f'{self._value}%')
        self.update()

    def setMinimumHeight(self, height):
        self.setFixedHeight(height)
        return self

    def _animate(self):
        self.offset += 5
        self.update()

    def paintEvent(self, event):
        painter = QPainter(self)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)
        w, h = self.width(), self.height()
        painter.fillRect(0, 0, w, h, QColor(240, 240, 240))
        wave_width = self._value / 100 * w
        path = QPainterPath()
        path.moveTo(0, h)
        for x in range(0, int(wave_width) + 10, 10):
            y = h - 20 + 10 * (x / 30 + self.offset / 20)
            path.lineTo(x, y)
        path.lineTo(wave_width, h)
        path.closeSubpath()
        gradient = QLinearGradient(0, 0, wave_width, 0)
        gradient.setColorAt(0, QColor(76, 217, 100))
        gradient.setColorAt(1, QColor(52, 199, 89))
        painter.fillPath(path, gradient)
        painter.setFont(QFont("Arial", 12, QFont.Weight.Bold))
        painter.setPen(QColor(51, 51, 51))
        if self._format:
            display_text = self._format
        else:
            display_text = f"{self._value}%"
        painter.drawText(0, 0, w, h, Qt.AlignmentFlag.AlignCenter, display_text)

class DotProgressBar(QWidget):
    """点阵进度条"""
    def __init__(self, parent=None):
        super().__init__(parent)
        self._value = 0
        self.setFixedHeight(30)
        self._format = ""
        self.setValue(0)

    def setValue(self, value):
        self._value = value
        self.update()

    def value(self):
        return self._value

    def setMinimum(self, value):
        pass

    def setMaximum(self, value):
        pass

    def setFormat(self, format_str):
        self._format = format_str
        self.update()

    def setMinimumHeight(self, height):
        self.setFixedHeight(height)
        return self

    def paintEvent(self, event):
        painter = QPainter(self)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)
        w, h = self.width(), self.height()
        # ===== 根据宽度自适应点阵数量 =====
        dot_radius = 6
        dot_spacing = dot_radius * 3  # 点间距 = 半径 * 3
        dot_count = max(10, int((w - 20) / dot_spacing))  # 最少10个点,根据宽度计算
        # 重新计算实际间距,让点均匀分布
        total_width = w - 20  
        actual_spacing = total_width / max(dot_count, 1)
        start_x = 10  # 起始X坐标
        filled = int(self._value / 100 * dot_count)
        for i in range(dot_count):
            x = start_x + i * actual_spacing + actual_spacing / 2
            y = h / 2
            radius = dot_radius
            if i < filled:
                gradient = QRadialGradient(x, y, radius)
                gradient.setColorAt(0, QColor(76, 217, 100))
                gradient.setColorAt(1, QColor(52, 199, 89))
                painter.setBrush(gradient)
                painter.setPen(Qt.PenStyle.NoPen)
                painter.drawEllipse(QPointF(x, y), radius, radius)
            else:
                painter.setBrush(QColor(200, 200, 200))
                painter.setPen(Qt.PenStyle.NoPen)
                painter.drawEllipse(QPointF(x, y), radius, radius)
        # 显示百分比(靠右)
        painter.setFont(QFont("Arial", 9))
        painter.setPen(QColor(51, 51, 51))

class GreenProgressBar(QProgressBar):
    """薄荷绿进度条"""
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setMinimumHeight(20)
        self.setStyleSheet("""
            QProgressBar {
                border: none;
                border-radius: 10px;
                background-color: #c8e6c9;
                text-align: center;
                color: #1b5e20;
                font-weight: bold;
            }
            QProgressBar::chunk {
                border-radius: 10px;
                background-color: #4caf50;
            }
        """)
        self.setValue(0)

    def setValue(self, value):
        super().setValue(value)
        self.setFormat(f"🌿 {value}%")

class PinkProgressBar(QProgressBar):
    """樱花粉进度条"""
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setMinimumHeight(20)
        self.setStyleSheet("""
            QProgressBar {
                border: none;
                border-radius: 10px;
                background-color: #f8bbd0;
                text-align: center;
                color: #880e4f;
                font-weight: bold;
            }
            QProgressBar::chunk {
                border-radius: 10px;
                background-color: #e91e63;
            }
        """)

    def setValue(self, value):
        super().setValue(value)
        self.setFormat(f"🌸 {value}%")

class PurpleProgressBar(QProgressBar):
    """星际紫进度条"""
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setMinimumHeight(20)
        self.setStyleSheet("""
            QProgressBar {
                border: none;
                border-radius: 10px;
                background-color: #e9d5ff;
                text-align: center;
                color: #4a1a7a;
                font-weight: bold;
            }
            QProgressBar::chunk {
                border-radius: 10px;
                background: qlineargradient(
                    x1: 0, y1: 0, x2: 1, y2: 0,
                    stop: 0 #9333ea,
                    stop: 1 #a855f7
                );
            }
        """)
        self.setValue(0)

    def setValue(self, value):
        super().setValue(value)
        self.setFormat(f"🌌 {value}%")

class BlueProgressBar(QProgressBar):
    """深海蓝进度条"""
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setMinimumHeight(20)
        self.setStyleSheet("""
            QProgressBar {
                border: none;
                border-radius: 10px;
                background-color: #bbdefb;
                text-align: center;
                color: #0d47a1;
                font-weight: bold;
            }
            QProgressBar::chunk {
                border-radius: 10px;
                background: qlineargradient(
                    x1: 0, y1: 0, x2: 1, y2: 0,
                    stop: 0 #2196f3,
                    stop: 1 #42a5f5
                );
            }
        """)
        self.setValue(0)

    def setValue(self, value):
        super().setValue(value)
        self.setFormat(f"🌊 {value}%")

class CollapsibleBox(QWidget):
    def __init__(self, title="", parent=None):
        super().__init__(parent)
        self.is_collapsed = False
        layout = QVBoxLayout(self)
        layout.setContentsMargins(0,0,0,0)
        layout.setSpacing(0)
        self.header = QFrame()
        self.header.setFrameStyle(QFrame.Shape.StyledPanel | QFrame.Shadow.Plain)
        self.header.setStyleSheet("QFrame{background-color:#f0f0f0;border:1px solid #ccc;border-radius:4px}")
        hl = QHBoxLayout(self.header)
        hl.setContentsMargins(8,4,8,4)
        self.toggle_btn = QToolButton()
        self.toggle_btn.setText(f"▶ {title}")
        self.toggle_btn.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
        self.toggle_btn.clicked.connect(self.toggle)
        hl.addWidget(self.toggle_btn)
        hl.addStretch()
        layout.addWidget(self.header)
        self.content_area = QWidget()
        self.content_layout = QVBoxLayout(self.content_area)
        self.content_layout.setContentsMargins(8,4,8,4)
        self.content_layout.setSpacing(4)
        layout.addWidget(self.content_area)

    def toggle(self):
        self.is_collapsed = not self.is_collapsed
        self.content_area.setVisible(not self.is_collapsed)
        t = self.toggle_btn.text().replace("▶ ","").replace("▼ ","")
        self.toggle_btn.setText(f"{'▼' if not self.is_collapsed else '▶'} {t}")

    def add_widget(self, w): self.content_layout.addWidget(w)

    def add_layout(self, l): self.content_layout.addLayout(l)

class EmojiButton(QPushButton):
    def __init__(self, text="", parent=None):
        super().__init__(text, parent)
        font_name = "Segoe UI Emoji" if sys.platform == "win32" else (
            "Apple Color Emoji" if sys.platform == "darwin" else "Noto Color Emoji")
        self.setFont(QFont(font_name, 10))
        self.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
        self.setMinimumWidth(60)
        self._tooltip_text = ""
        self.setMouseTracking(True)

    def setToolTip(self, text):
        self._tooltip_text = text
        super().setToolTip(text)

    def enterEvent(self, event):
        super().enterEvent(event)
        if self._tooltip_text:
            # 立即显示,不延迟
            QToolTip.showText(event.globalPosition().toPoint(), self._tooltip_text, self)

    def leaveEvent(self, event):
        super().leaveEvent(event)
        QToolTip.hideText()

class DragDropLineEdit(QLineEdit):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setPlaceholderText("可拖拽文件到此处...")
        self._drag_enabled = False

    def enable_drag_drop(self):
        """延迟启用拖拽功能"""
        if not self._drag_enabled:
            self.setAcceptDrops(True)
            self._drag_enabled = True

    def dragEnterEvent(self, e):
        if self._drag_enabled and e.mimeData().hasUrls():
            e.acceptProposedAction()

    def dropEvent(self, e):
        if not self._drag_enabled:
            return
        urls = e.mimeData().urls()
        if urls:
            path = urls[0].toLocalFile()
            normalized = os.path.normpath(path)
            self.setText(normalized)
            self.textChanged.emit(normalized)

class LogTextEdit(QPlainTextEdit):
    files_dropped = pyqtSignal(list)
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setReadOnly(True)
        self.setAcceptDrops(True)
        self.setPlaceholderText("可将数据文件拖拽到此区域自动添加\n\n打包日志将显示在这里...")
        # ========== 添加滚动条 ==========
        self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
        # 可选:设置滚动条样式
        self.setStyleSheet("""
                    QPlainTextEdit {
                        font-family: Consolas;
                        font-size: 12px;
                    }
                    QScrollBar:vertical {
                        background: #f0f0f0;
                        width: 12px;
                    }
                    QScrollBar::handle:vertical {
                        background: #c0c0c0;
                        min-height: 20px;
                        border-radius: 6px;
                    }
                """)

    def append_log(self, msg):
        """添加日志到GUI(线程安全)"""
        try:
            self.appendPlainText(msg)
            scrollbar = self.verticalScrollBar()
            scrollbar.setValue(scrollbar.maximum())
            QApplication.processEvents()
        except Exception as e:
            pass

    def dragEnterEvent(self, e):
        if e.mimeData().hasUrls():
            e.acceptProposedAction()
        else:
            super().dragEnterEvent(e)

    def dragMoveEvent(self, e):
        if e.mimeData().hasUrls():
            e.acceptProposedAction()
        else:
            super().dragMoveEvent(e)

    def dropEvent(self, e):
        urls = e.mimeData().urls()
        if urls:
            files = []
            for u in urls:
                path = u.toLocalFile()
                if path and os.path.exists(path):
                    files.append(path)
                    self.append_log(f"📎 添加文件: {os.path.basename(path)}")
            if files:
                self.files_dropped.emit(files)
            e.acceptProposedAction()
        else:
            super().dropEvent(e)

class PackageWorker(QThread):
    log_signal = pyqtSignal(str)
    progress_signal = pyqtSignal(int)
    time_signal = pyqtSignal(str)
    finished_signal = pyqtSignal(bool, str)
    def __init__(self, config):
        super().__init__()
        self.config = config
        self._is_running = True
        self.start_time = None
        self.process = None
        self._use_msvc = False  # 添加MSVC标志
        self._downloading = False  # 添加下载标志
        # 日志标志,防止重复打印
        self._cache_logged = False
        self._mingw_logged = False
        self._local_logged = False
        self._incomplete_logged = False
        self._msvc_logged = False
        self._msvc_ok_logged = False
        self._download_logged = False

    def safe_log(self, msg):
        """兼容方法,发送日志信号"""
        self.log_signal.emit(msg)

    def run(self):
        process = None
        packer = self.config.get('packer', 'PyInstaller')
        try:
            self.log_signal.emit("🚀 开始打包...")
            '''
            if packer == 'Nuitka':
                self._run_nuitka_pack(self.config)
                return
            '''
            cmd = self._build_command()
            target_python = self.config.get('target_python', sys.executable)
            use_venv = self.config.get('use_venv', False)
            # ===== 构建环境变量 =====
            if use_venv:
                # ===== 虚拟环境模式:彻底隔离,只用自己的 =====
                env = {}
                # 1. 系统变量
                system_keys = [
                    'SYSTEMROOT', 'TEMP', 'TMP', 'USERPROFILE',
                    'HOMEDRIVE', 'HOMEPATH', 'COMSPEC', 'WINDIR',
                    'ProgramFiles', 'CommonProgramFiles', 'ALLUSERSPROFILE'
                ]
                for key in system_keys:
                    if key in os.environ:
                        env[key] = os.environ[key]
                # 2. PATH:虚拟环境自己的目录
                python_dir = os.path.dirname(target_python)
                path_dirs = [
                    python_dir,  # 虚拟环境根目录
                    os.path.join(python_dir, 'Scripts'),  # 虚拟环境Scripts
                ]
                # 加上Windows系统目录(必要的)
                system_paths = [
                    r'C:\Windows\System32',
                    r'C:\Windows',
                    r'C:\Windows\System32\Wbem',
                    r'C:\Windows\System32\WindowsPowerShell\v1.0',
                ]
                for p in system_paths:
                    if os.path.exists(p) and p not in path_dirs:
                        path_dirs.append(p)
                env['PATH'] = os.pathsep.join(path_dirs)
                # 3. 构建 PYTHONPATH(只包含虚拟环境自己的路径,不包含系统)
                pythonpath_dirs = []
                venv_site_packages = self.config.get('venv_site_packages')
                if venv_site_packages and os.path.exists(venv_site_packages):
                    pythonpath_dirs.append(venv_site_packages)
                # Python 标准库 Lib(虚拟环境自己的)
                python_lib = os.path.join(python_dir, 'Lib')
                if os.path.exists(python_lib) and python_lib not in pythonpath_dirs:
                    pythonpath_dirs.append(python_lib)
                # Python DLLs(虚拟环境自己的)
                python_dlls = os.path.join(python_dir, 'DLLs')
                if os.path.exists(python_dlls) and python_dlls not in pythonpath_dirs:
                    pythonpath_dirs.append(python_dlls)
                # Python 根目录(虚拟环境自己的)
                if os.path.exists(python_dir) and python_dir not in pythonpath_dirs:
                    pythonpath_dirs.append(python_dir)
                if pythonpath_dirs:
                    env['PYTHONPATH'] = os.pathsep.join(pythonpath_dirs)
                # 4. 阻止访问系统
                env['PYTHONNOUSERSITE'] = '1'
                env['PYTHONSAFEPATH'] = '1'
                # 5. 清除系统Python变量
                for key in ['PYTHONHOME', 'VIRTUAL_ENV', 'PYTHONPATH_OLD',
                            'PYTHONSTARTUP', 'PYTHONEXECUTABLE']:
                    env.pop(key, None)
                # 6. 编码
                env['PYTHONIOENCODING'] = 'utf-8'
                env['PYTHONUTF8'] = '1'
                if sys.platform == 'win32':
                    env['PYTHONLEGACYWINDOWSSTDIO'] = 'utf-8'
                self.log_signal.emit(f"📁 使用虚拟环境隔离模式: {target_python}")
            else:
                # ===== 非虚拟环境:使用当前环境,只清理干扰项 =====
                env = os.environ.copy()
                env['PYTHONIOENCODING'] = 'utf-8'
                env['PYTHONUTF8'] = '1'
                if sys.platform == 'win32':
                    env['PYTHONLEGACYWINDOWSSTDIO'] = 'utf-8'
                env.pop('PYTHONHOME', None)
            # 打印命令
            if 'response_file' in self.config and self.config['response_file']:
                try:
                    with open(self.config['response_file'], 'r', encoding='utf-8') as f:
                        content = f.read()
                except:
                    pass
            startupinfo = get_startupinfo()
            process = self._popen_hidden(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                text=True,
                encoding="utf-8",
                errors="replace",
                startupinfo=startupinfo,
                bufsize=1,
                universal_newlines=True,
                creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0,
                env=env
            )
            self.process = process
            for line in iter(process.stdout.readline, ""):
                if not self._is_running:
                    process.terminate()
                    import time
                    time.sleep(0.3)
                    if process.poll() is None:
                        process.kill()
                    return
                if line:
                    line = line.rstrip()
                    if line:
                        try:
                            self.log_signal.emit(line)
                        except:
                            self.log_signal.emit(
                                line.encode('utf-8', errors='replace').decode('utf-8', errors='replace'))
                        try:
                            p = self._parse_progress(line)
                            if p is not None:
                                self.progress_signal.emit(p)
                        except:
                            pass
            process.wait()
            returncode = process.returncode
            if returncode == 0:
                try:
                    upx_result = self._manual_upx_compress()
                    if upx_result:
                        self.log_signal.emit(f"✅ {upx_result}")
                except Exception as e:
                    self.log_signal.emit(f"⚠️ UPX压缩异常: {e}")
                self.finished_signal.emit(True, "打包完成!")
            else:
                self.finished_signal.emit(False, f"返回码: {returncode}")
        except Exception as e:
            self.finished_signal.emit(False, str(e))
        finally:
            if hasattr(self, '_temp_packers_installed') and self._temp_packers_installed:
                venv_python = self.config.get('venv_python')
                if venv_python and os.path.exists(venv_python):
                    for pkg in self._temp_packers_installed:
                        if pkg in ['pyinstaller', 'nuitka']:
                            continue
                        self._uninstall_temp_packer(venv_python, pkg)
                self._temp_packers_installed = []
            if process:
                try:
                    if process.poll() is None:
                        process.terminate()
                        import time
                        time.sleep(0.5)
                        if process.poll() is None:
                            process.kill()
                            time.sleep(0.2)
                except:
                    pass
                try:
                    if process.stdout:
                        process.stdout.close()
                except:
                    pass
                try:
                    if process.stderr:
                        process.stderr.close()
                except:
                    pass
            self.process = None
            import gc
            gc.collect()

    def _manual_upx_compress(self):
        try:
            upx_path = self.config.get('upx_path', '')
            if not upx_path or not os.path.exists(upx_path):
                return None
            compress_level = self.config.get('compress_level', '默认')
            if compress_level == '不压':
                return None
            # 获取exe路径
            exe_path = self._get_exe_path()
            if not exe_path or not os.path.exists(exe_path):
                return None
            # ===== 确保UPX在PATH中 =====
            upx_dir = os.path.dirname(upx_path)
            current_path = os.environ.get('PATH', '')
            if upx_dir not in current_path:
                os.environ['PATH'] = upx_dir + os.pathsep + current_path
            # 清空环境变量
            os.environ.pop('UPX', None)
            os.environ.pop('UPX_FLAGS', None)
            # UPX 参数
            upx_args = {
                '最快': '-1',
                '默认': '-7',
                '最好': '--best',
                '极致': '--ultra-brute'
            }.get(compress_level, '-7')
            upx_args = f'{upx_args} --force'
            original_size = os.path.getsize(exe_path)
            try:
                self._run_hidden(
                    [upx_path, '-d', exe_path],
                    capture_output=True, timeout=60,
                    startupinfo=get_startupinfo()
                )
            except:
                pass
            # ===== 执行压缩 =====
            result = self._run_hidden(
                [upx_path] + upx_args.split() + [exe_path],
                capture_output=True, text=True, timeout=300,
                startupinfo=get_startupinfo()
            )
            if result.returncode == 0:
                new_size = os.path.getsize(exe_path)
                saved = original_size - new_size
                saved_percent = (saved / original_size * 100) if original_size > 0 else 0
            else:
                return None
        except subprocess.TimeoutExpired:
            return None
        except Exception as e:
            self.log_signal.emit(f"⚠️ UPX异常: {e}")
            return None

    def __del__(self):
        """析构函数,确保进程被清理"""
        if hasattr(self, 'process') and self.process:
            try:
                if self.process.poll() is None:
                    self.process.terminate()
                    import time
                    time.sleep(0.3)
                    if self.process.poll() is None:
                        self.process.kill()
                if self.process.stdout:
                    self.process.stdout.close()
                if self.process.stderr:
                    self.process.stderr.close()
            except:
                pass
            self.process = None
            import gc
            gc.collect()

    def _run_hidden(self, args, **kwargs):
        """隐藏窗口运行命令(兼容所有调用)"""
        if sys.platform == 'win32':
            if 'startupinfo' not in kwargs:
                si = subprocess.STARTUPINFO()
                si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
                si.wShowWindow = subprocess.SW_HIDE
                kwargs['startupinfo'] = si
            if 'creationflags' not in kwargs:
                kwargs['creationflags'] = subprocess.CREATE_NO_WINDOW
        return subprocess.run(args, **kwargs)

    def _popen_hidden(self, args, **kwargs):
        """隐藏窗口运行命令(Popen)- 支持编码"""
        if sys.platform == 'win32':
            if 'startupinfo' not in kwargs:
                si = subprocess.STARTUPINFO()
                si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
                si.wShowWindow = subprocess.SW_HIDE
                kwargs['startupinfo'] = si
            if 'creationflags' not in kwargs:
                kwargs['creationflags'] = subprocess.CREATE_NO_WINDOW
            if 'text' not in kwargs and 'encoding' not in kwargs:
                kwargs['text'] = True
                kwargs['encoding'] = 'utf-8'
                kwargs['errors'] = 'replace'
        return subprocess.Popen(args, **kwargs)

    def _get_exe_path(self):
        """获取生成的 exe 路径"""
        try:
            script = self.config.get('script', '')
            if not script:
                return None
            project_name = os.path.splitext(os.path.basename(script))[0]
            output_dir = self.config.get('output', os.path.join(os.path.dirname(script), 'dist'))
            possible_paths = [
                os.path.join(output_dir, f'{project_name}.exe'),
                os.path.join(output_dir, project_name, f'{project_name}.exe'),
                os.path.join(os.path.dirname(script), 'dist', f'{project_name}.exe'),
                os.path.join(os.path.dirname(script), 'dist', project_name, f'{project_name}.exe'),
            ]
            for path in possible_paths:
                if os.path.exists(path):
                    return path
            return None
        except:
            return None

    def _format_size(self, size):
        """格式化大小显示"""
        try:
            if size <= 0:
                return "0 B"
            if size < 1024:
                return f"{int(size)} B"
            elif size < 1024 * 1024:
                return f"{size / 1024:.2f} KB ({int(size)} B)"
            elif size < 1024 * 1024 * 1024:
                return f"{size / (1024 * 1024):.2f} MB ({int(size / 1024):,} KB)"
            else:
                return f"{size / (1024 * 1024 * 1024):.3f} GB ({int(size / (1024 * 1024)):,} MB)"
        except Exception:
            return "0 B"

    def _build_command(self):
        cfg = self.config
        cmd = []
        packer = cfg.get('packer', 'PyInstaller')
        script = cfg.get('script', '')
        use_venv = cfg.get('use_venv', False)
        target_python = cfg.get('target_python', sys.executable)
        venv_python = cfg.get('venv_python')
        version_file = cfg.get('version_file')
        version_info = cfg.get('version_info', {})
        # ===== 获取所有选项 =====
        platform = cfg.get('platform', 'current')
        log_level = cfg.get('log_level', 'INFO')
        collect = cfg.get('collect', '')
        copy_metadata = cfg.get('copy_metadata', '')
        # ===== 虚拟模式:处理临时打包器 =====
        if venv_python and os.path.exists(venv_python):
            packer_map = {
                'PyInstaller-spec': 'pyinstaller',
                'PyInstaller-cmd': 'pyinstaller',
                'Nuitka': 'nuitka',
                'PyApp': 'pyapp',
                'Py2exe': 'py2exe',
                'Cx_Freeze': 'cx-freeze',
                'Pynsist': 'pynsist',
                'PyOxidizer': 'pyoxidizer',
                'Py2app': 'py2app',
            }
            packer_name = packer_map.get(packer)
            if packer_name and packer_name not in ['pyinstaller', 'nuitka']:
                self._install_temp_packer(venv_python, packer_name)
                if not hasattr(self, '_temp_packers_installed'):
                    self._temp_packers_installed = []
                if packer_name not in self._temp_packers_installed:
                    self._temp_packers_installed.append(packer_name)

        def quote_path(p):
            if not p:
                return p
            if ' ' in p:
                return f'"{p}"'
            return p
        # ========== PyInstaller-spec 模式 ==========
        if packer == 'PyInstaller-spec' or script.lower().endswith('.spec'):
            spec_dir = os.path.dirname(script)
            build_dir = os.path.join(spec_dir, 'build')
            if use_venv:
                cmd = [target_python, '-S', '-m', 'PyInstaller',
                       '--distpath', spec_dir,
                       '--workpath', build_dir]
            else:
                cmd = [target_python, '-m', 'PyInstaller',
                       '--distpath', spec_dir,
                       '--workpath', build_dir]
            compress_level = cfg.get('compress_level', '默认')
            upx_path = cfg.get('upx_path', '')
            if upx_path and os.path.exists(upx_path) and compress_level != '不压':
                upx_dir = os.path.dirname(upx_path)
                cmd.append('--upx-dir')
                cmd.append(upx_dir)
                current_path = os.environ.get('PATH', '')
                if upx_dir not in current_path:
                    os.environ['PATH'] = upx_dir + os.pathsep + current_path
                UPX_FLAGS = ''
                if compress_level == '最快':
                    os.environ['UPX_FLAGS'] = '-1'
                elif compress_level == '默认':
                    os.environ['UPX_FLAGS'] = '-7'
                elif compress_level == '最好':
                    os.environ['UPX_FLAGS'] = '--best'
                elif compress_level == '极致':
                    os.environ['UPX_FLAGS'] = '--ultra-brute'
                self.log_signal.emit(f"🗜️ UPX压缩: {compress_level}模式 {os.environ['UPX_FLAGS']} ")
            if cfg.get('clean', False):
                cmd.append('--clean')
            if cfg.get('extra_args'):
                extra = cfg['extra_args']
                if isinstance(extra, str):
                    cmd.extend(extra.split())
                else:
                    cmd.extend(extra)
            cmd.append(script)
            return cmd
        # ========== Nuitka 模式 ==========
        if packer == 'Nuitka':
            cmd = [target_python, '-m', 'nuitka']
            if version_info:
                if version_info.get('product_name'):
                    cmd.append(f'--product-name={version_info["product_name"]}')
                if version_info.get('company'):
                    cmd.append(f'--company-name={version_info["company"]}')
                if version_info.get('file_version'):
                    cmd.append(f'--file-version={version_info["file_version"]}')
                if version_info.get('product_version'):
                    cmd.append(f'--product-version={version_info["product_version"]}')
                if version_info.get('product_name'):
                    cmd.append(f'--file-description={version_info["product_name"]}')
                if version_info.get('company'):
                    cmd.append(f'--copyright=Copyright (c) {datetime.datetime.now().year} {version_info["company"]}')
                #self.safe_log(f"📋 已应用版本信息: {version_info.get('product_name', '')} v{version_info.get('product_version', '')}")
            compat_mode = cfg.get('nuitka_compat', False)
            backend = cfg.get('backend', 'auto')
            mingw_path = cfg.get('mingw_path', '')
            msvc_path = cfg.get('msvc_path', '')
            has_mingw = cfg.get('has_mingw', False)
            has_msvc = cfg.get('has_msvc', False)
            optimize = cfg.get('optimize', '平衡')
            disable_ccache = cfg.get('disable_ccache', False)
            if not disable_ccache:
                ccache_path = self._find_best_ccache()
                if ccache_path:
                    os.environ['NUITKA_CCACHE_BINARY'] = ccache_path
                    self.safe_log(f"✅ ccache: {ccache_path}")
                else:
                    self.safe_log("⚠️ 未找到ccache")
            else:
                self.safe_log("🚫 已禁用ccache")
            # ===== 【直接使用 cfg 中的 excludes】 =====
            hidden_imports = cfg.get('hidden_imports', [])
            exclude_list = cfg.get('excludes', [])
            if exclude_list:
                # 用逗号分隔所有排除的包
                cmd.append(f'--nofollow-import-to={",".join(exclude_list)}')
                #self.safe_log(f"🚫 排除 {len(exclude_list)} 个包")
            # ===== 输出模式 =====
            if cfg.get('onefile', True):
                if compat_mode:
                    cmd.append('--standalone')
                    cmd.append('--onefile')
                else:
                    cmd.append('--onefile')
            else:
                cmd.append('--standalone')
            # ===== 控制台 =====
            if not cfg.get('debug', False):
                if compat_mode:
                    cmd.append('--windows-console-mode=disable')
                else:
                    cmd.append('--disable-console')
            else:
                if compat_mode:
                    cmd.append('--windows-console-mode=attach')
            # ===== 名称和输出 =====
            if cfg.get('name'):
                cmd.append(f'--output-filename={cfg["name"]}')
            if cfg.get('output'):
                cmd.append(f'--output-dir={cfg["output"]}')
            # ===== 图标 =====
            if cfg.get('icon'):
                icon_path = cfg['icon']
                icon_name = os.path.basename(icon_path)
                if compat_mode:
                    cmd.append(f'--windows-icon-from-ico={icon_path}')
                else:
                    cmd.append(f'--icon={icon_path}')
                cmd.append(f'--include-data-file={icon_path}={icon_name}')
            # ===== 并行编译 =====
            jobs = cfg.get('jobs', 'auto')
            if jobs == 'auto':
                import multiprocessing
                auto_jobs = max(1, multiprocessing.cpu_count())
                cmd.append(f'--jobs={auto_jobs}')
                self.safe_log(f"🔧 自动并行编译: {auto_jobs} 核")
            else:
                cmd.append(f'--jobs={jobs}')
                self.safe_log(f"🔧 并行编译: {jobs} 核")
            # ===== 编译器后端 =====
            backend = cfg.get('backend', 'auto')
            if backend == 'auto':
                if has_mingw:
                    cmd.append('--mingw64')
                elif has_msvc:
                    cmd.append('--msvc=latest')
                else:
                    cmd.append('--mingw64')
            elif backend == 'MinGW64':
                cmd.append('--mingw64')
            elif backend == 'MSVC':
                cmd.append('--msvc=latest')
            # ===== GUI插件 =====
            plugin = cfg.get('gui_plugin', 'auto')
            # if plugin != 'auto' and plugin != 'none':
            #     cmd.append(f'--enable-plugin={plugin}')
            # ===== LTO =====
            lto = cfg.get('lto', 'no')
            if optimize == "速度优先":
                if lto == 'yes' or lto == 'thin':
                    self.safe_log("⚡ 速度优先模式:禁用LTO")
                    lto = 'no'
            if lto == 'yes':
                cmd.append('--lto=yes')
                self.safe_log("🔗 已启用LTO 优化")
            elif lto == 'thin':
                cmd.append('--lto=thin')
                self.safe_log("🔗 已启用Thin LTO 优化")
            else:
                cmd.append('--lto=no')
                self.safe_log("🔗 LTO 已禁用")
            # ===== 隐藏导入自动检测插件 =====
            has_qt = any(mod.lower() in ['pyqt6', 'pyqt5', 'pyside6', 'pyside2'] for mod in hidden_imports)
            has_sf = any(mod.lower() in ['torch', 'numpy', 'pandas', 'matplotlib', 'tensorflow'] for mod in hidden_imports)
            has_tk = any(mod.lower() == 'tkinter' for mod in hidden_imports)
            has_wx = any(mod.lower() == 'wx' for mod in hidden_imports)
            plugin_args = set()
            if has_sf:
                for mod in hidden_imports:
                    if mod.lower() == 'torch':
                        plugin_args.add('--enable-plugin=torch')
                    elif mod.lower() == 'tensorflow':
                        plugin_args.add('--enable-plugin=tensorflow')
                    elif mod.lower() == 'numpy' or mod.lower() == 'pandas' or mod.lower() == 'matplotlib' :
                        plugin_args.add('--enable-plugin=numpy')                  
            if has_qt:  
                plugin_args.add('--include-qt-plugins=platforms,styles,imageformats')
                for mod in hidden_imports:            
                    if mod.lower() == 'pyqt6':
                        plugin_args.add('--enable-plugin=pyqt6')
                    elif mod.lower() == 'pyqt5':
                        plugin_args.add('--enable-plugin=pyqt5')
                    elif mod.lower() == 'pyside6':
                        plugin_args.add('--enable-plugin=pyside6')
                    elif mod.lower() == 'pyside2':
                        plugin_args.add('--enable-plugin=pyside2')              
            if has_tk:
                plugin_args.add('--enable-plugin=tk-inter')
            if has_wx:
                plugin_args.add('--enable-plugin=wx-python')
            cmd.extend(list(plugin_args))
            # ===== UPX压缩 =====
            upx_path = cfg.get('upx_path', '')
            compress_level = cfg.get('compress_level', '默认')
            if optimize == "速度优先":
                upx_enabled = False
                self.safe_log("⚡ 速度优先模式:禁用UPX压缩")
            else:
                upx_enabled = upx_path and os.path.exists(upx_path) and compress_level != '不压'
            if upx_enabled:
                if 'UPX' in os.environ:
                    del os.environ['UPX']
                if sys.platform == 'win32':
                    try:
                        import ctypes
                        GetShortPathName = ctypes.windll.kernel32.GetShortPathNameW
                        GetShortPathName.argtypes = [ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint32]
                        buffer = ctypes.create_unicode_buffer(260)
                        GetShortPathName(upx_path, buffer, 260)
                        short_path = buffer.value
                        if short_path:
                            upx_path = short_path
                    except:
                        pass
                cmd.append('--enable-plugin=upx')
                cmd.append(f'--upx-binary={upx_path}')
                if compress_level == '最好':
                    cmd.append('--optimize=1')
                elif compress_level == '极致':
                    cmd.append('--optimize=2')
                elif compress_level == '最快':
                    cmd.append('--optimize=0')
                self.safe_log(f"🗜️ UPX压缩: {compress_level}模式")
            # ===== 兼容模式 =====
            if compat_mode:
                cmd.append('--assume-yes-for-downloads')
            #进度\内存
            #cmd.append('--show-progress')
            #cmd.append('--show-memory')
            # ===== 去除符号 =====
            if cfg.get('strip', True):
                if optimize == "速度优先":
                    self.safe_log("⚡ 速度优先模式:保留调试符号以加快链接")
                else:
                    cmd.append('--remove-output')
            else:
                if not compat_mode:
                    cmd.append('--unstripped')
            # ===== 低内存 =====
            if cfg.get('low_memory', False):
                cmd.append('--low-memory')
                self.safe_log("🧠 已启用低内存模式")
            # ===== 实验性 =====
            if cfg.get('experimental', False):
                cmd.append('--experimental')
            # ===== 缓存目录(如果配置中有) =====
            cache_dir = cfg.get('cache_dir')
            if cache_dir:
                os.environ['NUITKA_CACHE_DIR'] = cache_dir
            # ===== 额外参数 =====
            if cfg.get('extra_args'):
                extra = cfg['extra_args']
                if isinstance(extra, str):
                    cmd.extend(extra.split())
                else:
                    cmd.extend(extra)
            # ===== 脚本 =====
            cmd.append(script)
            # ===== 打包外部脚本 =====
            pack_scripts = cfg.get('pack_scripts', [])
            for script_path in pack_scripts:
                if os.path.exists(script_path):
                    script_name = os.path.basename(script_path)
                    cmd.append(f'--include-data-file={script_path}={script_name}')
                    self.safe_log(f"📦 打包外部脚本: {script_name}")
            return cmd
        # ========== PyInstaller cmd 模式 ==========
        if use_venv:
            cmd = [target_python, '-S', '-m', 'PyInstaller']
        else:
            cmd = [target_python, '-m', 'PyInstaller']
        cmd.append('--onefile' if cfg.get('onefile', True) else '--onedir')
        # ===== 使用已生成的版本文件 =====
        if version_file and os.path.exists(version_file):
            cmd.append(f'--version-file={version_file}')
        for mod in cfg.get('hidden_imports', []):
            cmd.extend(['--hidden-import', mod])
        for mod in cfg.get('excludes', []):
            cmd.extend(['--exclude-module', mod])
        # ===== 添加日志级别 =====
        log_level = cfg.get('log_level', 'INFO')
        if log_level and log_level != 'INFO':
            cmd.extend(['--log-level', log_level])
        # 平台
        if platform and platform != 'current':
            cmd.extend(['--target-arch', platform])
        # 收集
        if collect:
            cmd.extend(['--collect-all', collect])
        # 元数据
        if copy_metadata:
            cmd.extend(['--copy-metadata', copy_metadata])
        if not cfg.get('debug', False):
            cmd.append('--noconsole')
        if cfg.get('clean', False):
            cmd.append('--clean')
        if cfg.get('strip', True):
            cmd.append('--strip')
        if cfg.get('name'):
            cmd.extend(['--name', cfg['name']])
        if cfg.get('output'):
            cmd.extend(['--distpath', cfg['output']])
        if cfg.get('icon'):
            cmd.extend(['--icon', cfg['icon']])
        compress_level = cfg.get('compress_level', '默认')
        upx_path = cfg.get('upx_path', '')
        if upx_path and os.path.exists(upx_path) and compress_level != '不压':
            upx_dir = os.path.dirname(upx_path)
            cmd.append('--upx-dir')
            cmd.append(upx_dir)
            current_path = os.environ.get('PATH', '')
            if upx_dir not in current_path:
                os.environ['PATH'] = upx_dir + os.pathsep + current_path
            UPX_FLAGS = ''
            if compress_level == '最快':
                os.environ['UPX_FLAGS'] = '-1'
            elif compress_level == '默认':
                os.environ['UPX_FLAGS'] = '-7'
            elif compress_level == '最好':
                os.environ['UPX_FLAGS'] = '--best'
            elif compress_level == '极致':
                os.environ['UPX_FLAGS'] = '--ultra-brute'
            self.log_signal.emit(f"🗜️ UPX压缩: {compress_level}模式 {os.environ['UPX_FLAGS']}")
        else:
            cmd.append('--noupx')
            if compress_level != '不压':
                self.log_signal.emit("⚠️ UPX未找到,使用 --noupx")
        for src, dst in cfg.get('data_files', []):
            sep = ';' if sys.platform == 'win32' else ':'
            cmd.extend(['--add-data', f'{src}{sep}{dst}'])
        if cfg.get('extra_args'):
            extra = cfg['extra_args']
            if isinstance(extra, str):
                cmd.extend(extra.split())
            else:
                cmd.extend(extra)
        cmd.append(script)
        pack_scripts = cfg.get('pack_scripts', [])
        for script_path in pack_scripts:
            if os.path.exists(script_path):
                script_name = os.path.basename(script_path)
                sep = ';' if sys.platform == 'win32' else ':'
                already = False
                for src, dst in cfg.get('data_files', []):
                    if src == script_path:
                        already = True
                        break
                if not already:
                    cmd.extend(['--add-data', f'{script_path}{sep}.'])
                    self.log_signal.emit(f"📦 打包外部脚本: {script_name}")
        return cmd

    def _fix_dir_permissions(dir_path):
        """递归修复目录下所有exe/dll文件的执行权限(Windows用icacls)"""
        if not os.path.isdir(dir_path):
            return
        try:
            subprocess.run(['icacls', dir_path, '/grant', 'Everyone:(OI)(CI)RX', '/T'],
                           capture_output=True, timeout=30)
        except:
            pass
        for root, dirs, files in os.walk(dir_path):
            for f in files:
                if f.endswith(('.exe', '.dll')):
                    p = os.path.join(root, f)
                    if os.path.isfile(p):
                        try:
                            subprocess.run(['powershell', '-Command', f'Unblock-File -Path "{p}"'],
                                           capture_output=True, timeout=5)
                        except:
                            pass
                        if not os.access(p, os.X_OK):
                            try:
                                os.chmod(p, 0o755)
                            except:
                                pass

    def _find_best_ccache(self, project_dir=None):
        """找版本最新的ccache,返回路径或None。不排除任何路径,自动修复Nuitka缓存权限。"""
        exe = 'ccache.exe' if sys.platform == 'win32' else 'ccache'
        candidates = []

        def _add_candidate(p):
            if not os.path.isfile(p):
                return
            if not os.access(p, os.X_OK):
                try:
                    os.chmod(p, 0o755)
                except:
                    pass
            if os.access(p, os.X_OK):
                candidates.append(p)
        for d in os.environ.get('PATH', '').split(os.pathsep):
            d = d.strip('"').strip("'")
            if d:
                _add_candidate(os.path.join(d, exe))
        if sys.platform == 'win32':
            try:
                r = subprocess.run(['where', 'ccache'], capture_output=True, text=True, timeout=5)
                if r.returncode == 0:
                    for line in r.stdout.strip().splitlines():
                        p = line.strip()
                        if 'Nuitka' in p and 'Cache' in p:
                            try:
                                # 向上找到Nuitka根目录(如 C:\...\Local\Nuitka)
                                nuitka_dir = p
                                while nuitka_dir and os.path.basename(nuitka_dir) != 'Nuitka':
                                    parent = os.path.dirname(nuitka_dir)
                                    if parent == nuitka_dir:
                                        break
                                    nuitka_dir = parent
                                # 如果上层也是Nuitka(如 Nuitka\Nuitka),取上层
                                if os.path.basename(nuitka_dir) == 'Nuitka':
                                    parent = os.path.dirname(nuitka_dir)
                                    if os.path.basename(parent) == 'Nuitka':
                                        nuitka_dir = parent
                                self._fix_dir_permissions(nuitka_dir)
                            except:
                                pass
                        _add_candidate(p)
            except:
                pass
        # which
        w = shutil.which('ccache')
        if w:
            _add_candidate(w)
        if not candidates:
            return None
        best_path, best_ver = None, (0,)
        for p in dict.fromkeys(candidates):
            try:
                if project_dir:
                    try:
                        if os.path.commonpath([os.path.normcase(os.path.abspath(p)),
                                               os.path.normcase(os.path.abspath(project_dir))]) == os.path.normcase(
                            os.path.abspath(project_dir)):
                            continue
                    except ValueError:
                        pass
                r = subprocess.run([p, '--version'], capture_output=True, text=True, timeout=5)
                if r.returncode != 0:
                    cdir = os.path.dirname(p)
                    env = os.environ.copy()
                    if cdir not in env.get('PATH', ''):
                        env['PATH'] = cdir + os.pathsep + env.get('PATH', '')
                    r = subprocess.run([p, '--version'], capture_output=True, text=True, timeout=5, cwd=cdir, env=env)
                if r.returncode == 0:
                    m = re.search(r'[Vv]ersion\s+(\d+)(?:\.(\d+))?(?:\.(\d+))?', r.stdout)
                    if not m:
                        m = re.search(r'ccache\s+(\d+)(?:\.(\d+))?(?:\.(\d+))?', r.stdout)
                    if not m:
                        m = re.search(r'(\d+)\.(\d+)(?:\.(\d+))?', r.stdout)
                    if m:
                        ver = tuple(int(x) if x else 0 for x in m.groups())
                        if ver > best_ver:
                            best_ver, best_path = ver, p
            except:
                continue
        return best_path if best_path else (candidates[0] if candidates else None)

    def _parse_progress(self, line):
        """解析真实进度 - 增加完成检测和通用百分比"""
        if not line or not isinstance(line, str):
            return None
        try:
            import re
            packer = self.config.get('packer')
            line_lower = line.lower()
            # ===== 优先检测完成信号(所有打包器通用) =====
            complete_keywords = [
                'completed successfully',
                'successfully completed',
                'building complete',
                'finished successfully',
                'done!',
                'exe built',
                'completed.',
                'success',
                'complete!',
                'finished!',
                'build complete',
                'successfully built',
            ]
            for kw in complete_keywords:
                if kw in line_lower:
                    return 100
            # ===== PyInstaller =====
            if packer.startswith('PyInstaller'):
                if 'INFO:' in line:
                    if "开始打包" in line:
                        return 5
                    if 'Analysis' in line:
                        return 10
                    if 'PYZ' in line:
                        return 30
                    if 'PKG' in line:
                        return 50
                    if 'EXE' in line:
                        return 80
                    if 'Complete' in line or 'completed' in line_lower:
                        return 95
                    if "打包完成" in line:
                        return 100   
            # ===== Nuitka =====
            elif packer == 'Nuitka':
                if 'Used command line options' in line:
                    return 1              
                if 'Starting Python compilation' in line:
                    return 5
                if 'Completed Python level compilation' in line or 'optimization' in line_lower:
                    return 10
                if 'Generating source code for C backend' in line:
                    return 20
                if 'Running data composer tool' in line:
                    return 30
                if 'Running C compilation via Scons' in line:
                    return 40
                if 'Backend C compiler' in line:
                    self.safe_log("⏳ 正在编译C代码,这可能需要较长时间...")
                    return 50
                if 'Slow C compilation detected' in line:
                    return 55
                if 'Backend C linking' in line:
                    return 60
                if 'Compiled' in line and 'C files' in line:
                    # 解析编译进度
                    match = re.search(r'Compiled\s+(\d+)\s+C files', line)
                    if match:
                        compiled = int(match.group(1))
                        # 假设总共约150个文件,计算进度
                        progress = min(50 + int(compiled / 150 * 30), 80)
                        return progress
                if 'Onefile: Creating single file' in line or 'Creating single file' in line:
                    return 85
                if 'Onefile payload compression' in line:
                    return 90
                if 'Onefile C linking' in line:
                    return 92
                if 'Removing onefile build directory' in line or 'removing onefile build' in line_lower:
                    return 95
                if 'Removing build directory' in line or 'removing build directory' in line_lower:
                    return 98
                if 'Successfully created' in line:
                    return 100
            # ===== Py2exe =====
            elif packer == 'Py2exe':
                if 'running' in line_lower or 'py2exe' in line_lower:
                    return 10
                if 'copying' in line_lower:
                    return 30
                if 'building' in line_lower:
                    return 50
                if 'dll' in line_lower:
                    return 70
                if 'complete' in line_lower:
                    return 100
            # ===== cx_Freeze =====
            elif packer == 'Cx_Freeze':
                if 'running build_exe' in line_lower:
                    return 10
                if 'running egg_info' in line_lower:
                    return 15
                if 'creating directory' in line_lower:
                    return 20
                if 'copying data from package' in line_lower:
                    return 30
                if 'copying' in line_lower and ('.pyd' in line_lower or '.dll' in line_lower):
                    return 50
                if 'writing zip file' in line_lower:
                    return 70
                if 'Missing dependencies' in line:
                    return 85
                if '打包完成' in line or 'cx_Freeze 打包完成' in line:
                    return 100
            # ===== PyApp =====
            elif packer == 'PyApp':
                if 'generating wheel' in line_lower or '正在生成' in line_lower:
                    return 10
                if 'wheel 包生成成功' in line_lower or 'wheel package' in line_lower:
                    return 20
                if '找到 cargo' in line_lower or 'found cargo' in line_lower:
                    return 25
                if 'fresh' in line_lower and ('unicode' in line_lower or 'proc-macro' in line_lower):
                    return 30
                if 'compiling' in line_lower and 'pyapp' in line_lower:
                    return 50
                if 'running' in line_lower and 'rustc' in line_lower:
                    return 60
                if 'finished release' in line_lower:
                    return 80
                if '已复制' in line_lower or 'copied' in line_lower:
                    return 90
                if '打包成功' in line_lower or 'success' in line_lower:
                    return 100
            # ===== PyOxidizer =====
            elif packer == 'PyOxidizer':
                if 'compiling' in line_lower:
                    return 20
                if 'linking' in line_lower:
                    return 60
                if 'finished' in line_lower:
                    return 80
                if 'success' in line_lower:
                    return 100
            # ===== Pynsist =====
            elif packer == 'Pynsist':
                if 'generating' in line_lower:
                    return 30
                if 'compiling' in line_lower:
                    return 60
                if 'success' in line_lower:
                    return 100
            # ===== Py2app =====
            elif packer == 'Py2app':
                if 'copying' in line_lower:
                    return 30
                if 'building' in line_lower:
                    return 60
                if 'creating' in line_lower:
                    return 80
                if 'complete' in line_lower:
                    return 100
            # ===== 通用百分比解析(兜底) =====
            match = re.search(r'(\d+)%', line)
            if match:
                pct = int(match.group(1))
                if 0 <= pct <= 100:
                    return pct
            # ===== 额外完成检测(兜底) =====
            if 'Building' in line and ('success' in line_lower or 'complete' in line_lower):
                return 100
            if 'Success' in line and ('exe' in line_lower or 'built' in line_lower):
                return 100
        except Exception as e:
            pass
        return None

    def stop(self):
        """停止打包"""
        self._is_running = False
        if self.process:
            packer = self.config.get('packer', '') if hasattr(self, 'config') else ''
            if packer == 'Nuitka':
                # Nuitka用terminate()优雅终止,
                try:
                    self.process.terminate()
                    try:
                        self.process.wait(timeout=2)
                    except subprocess.TimeoutExpired:
                        self.process.kill()
                        self.process.wait(timeout=1)
                except Exception:
                    pass
            else:
                try:
                    self.process.terminate()
                    try:
                        self.process.wait(timeout=2)
                    except subprocess.TimeoutExpired:
                        self.process.kill()
                except Exception:
                    pass
            self.process = None

    def _install_temp_packer(self, venv_python, packer_name):
        """临时安装打包器"""
        import subprocess
        clean_env = {'PATH': os.environ.get('PATH', '')}
        if sys.platform == 'win32':
            clean_env['SYSTEMROOT'] = os.environ.get('SYSTEMROOT', '')
        try:
            check = subprocess.run(
                [venv_python, '-m', 'pip', 'show', packer_name],
                capture_output=True, text=True, env=clean_env, timeout=5
            )
            if check.returncode == 0:
                return True
            self.log_signal.emit(f"📥 临时安装打包器: {packer_name}")
            result = subprocess.run(
                [venv_python, '-m', 'pip', 'install', packer_name, '-i', MIRROR, '-q', '--no-warn-script-location'],
                capture_output=True, text=True, env=clean_env, timeout=300
            )
            if result.returncode == 0:
                self.log_signal.emit(f"✅ {packer_name} 临时安装成功")
                return True
            else:
                self.log_signal.emit(f"❌ {packer_name} 安装失败: {result.stderr[:100]}")
                return False
        except Exception as e:
            self.log_signal.emit(f"❌ 临时安装异常: {e}")
            return False

    def _uninstall_temp_packer(self, venv_python, packer_name):
        """卸载临时打包器"""
        import subprocess
        if packer_name in ['pyinstaller', 'nuitka']:
            return
        clean_env = {'PATH': os.environ.get('PATH', '')}
        if sys.platform == 'win32':
            clean_env['SYSTEMROOT'] = os.environ.get('SYSTEMROOT', '')
        try:
            self.log_signal.emit(f"📤 卸载临时打包器: {packer_name}")
            subprocess.run(
                [venv_python, '-m', 'pip', 'uninstall', '-y', packer_name],
                capture_output=True, text=True, env=clean_env, timeout=60
            )
            self.log_signal.emit(f"✅ {packer_name} 已卸载")
        except Exception as e:
            self.log_signal.emit(f"⚠️ 卸载 {packer_name} 失败: {e}")
# ==================== ContentLoader ====================
class ContentLoader(QThread):
    finished = pyqtSignal(str, str)

    def __init__(self):
        super().__init__()

    def run(self):
        changelog = self._load_or_default("CHANGELOG.txt", self.get_default_changelog())
        tutorial = self._load_or_default("TUTORIAL.txt", self.get_default_tutorial())
        self.finished.emit(changelog, tutorial)

    def _load_or_default(self, filename, default_func):
        try:
            if os.path.exists(filename):
                with open(filename, 'r', encoding='utf-8') as f:
                    content = f.read().strip()
                    if content:
                        return content
        except:
            pass
        return default_func()

    @staticmethod
    def get_default_changelog():
        return "\n".join([
            "=" * 50,
            f"  Python 跨平台打包工具 v{VERSION}",
            "=" * 50,
            "",
            f"  更新日期: {BUILD_DATE}",
            f"  作者: {AUTHOR}",
            "",
            "-" * 40,
            "v7.0.0 - 重大更新",
            "-" * 40,
            "  * 支持在线获取GitHub源码",
            "  * 打包进度彩色显示 (8种风格)",
            "  * 语法检查与修复功能",
            "  * 大小预估功能",
            "  * 异步秒启动优化",
            "  * 完善约九种打包器支持",
            "",
            "  打包器支持:",
            "  - PyInstaller (cmd/spec)",
            "  - Nuitka (MinGW64/MSVC)",
            "  - Py2exe / Cx_Freeze",
            "  - PyApp / Pynsist",
            "  - PyOxidizer / Py2app",
            "",
            "  功能特性:",
            "  - 虚拟环境隔离打包",
            "  - UPX 压缩集成",
            "  - ccache 加速编译",
            "  - 系统资源实时监控",
            "  - 代码修复预览对比",
            "  - 多镜像自动切换",
            "  - 自动安装Python环境",
            "=" * 50,
        ])

    @staticmethod
    def get_default_tutorial():
        return "\n".join([
            "=" * 50,
            "    使用教程 - Python代码打包工具",
            "=" * 50,
            "",
            "第一步: 选择Python脚本",
            "  点击浏览按钮或拖拽.py文件到输入框。",
            "",
            "第二步: 选择打包器",
            "  - PyInstaller: 最流行,兼容性好,推荐新手",
            "  - Nuitka: 编译为C代码,性能好,体积小",
            "  - Py2exe / Cx_Freeze: 经典老牌工具",
            "",
            "第三步: 配置选项",
            "  - 单文件/目录模式: 推荐单文件",
            "  - 控制台: 调试时勾选,发布时取消",
            "  - 图标: 选择.ico文件作为程序图标",
            "  - 数据文件: 添加程序需要的额外文件",
            "",
            "第四步: 点击开始打包",
            "  等待进度条完成,在输出目录找到生成的exe。",
            "",
            "-" * 40,
            "  高级选项说明",
            "-" * 40,
            "",
            "  隐藏导入 (--hidden-import):",
            "  如果打包后提示缺少模块,在此添加模块名。",
            "",
            "  排除模块 (--exclude-module):",
            "  排除不需要的模块以减小体积。",
            "",
            "  UPX压缩:",
            "  UPX可对exe压缩30%-50%。",
            "  下载upx.exe放到工具目录即可自动检测。",
            "",
            "  虚拟环境:",
            "  在虚拟环境中安装打包器,避免系统污染。",
            "",
            "  Nuitka编译器:",
            "  - MinGW64: 自动下载,无需配置",
            "  - MSVC: 需要安装Visual Studio",
            "",
            "-" * 40,
            "  常见问题",
            "-" * 40,
            "",
            "Q: 打包后exe运行闪退?",
            "A: 勾选控制台模式重新打包,在cmd中运行查看错误。",
            "",
            "Q: 提示模块未找到?",
            "A: 在隐藏导入中添加该模块名。",
            "",
            "Q: 打包后体积太大?",
            "A: 使用虚拟环境+UPX压缩+排除无用模块。",
            "",
            "Q: Nuitka打包很慢?",
            "A: Nuitka需要编译C代码,首次较慢,",
            "   后续会使用ccache缓存加速。",
            "=" * 50,
        ])

class SyntaxCheckWorker(QThread):
    log_signal = pyqtSignal(str)
    finished_signal = pyqtSignal(bool, str, object)

    def __init__(self, script_path):
        super().__init__()
        self.script_path = script_path

    def run(self):
        try:
            with open(self.script_path, 'r', encoding='utf-8') as f:
                content = f.read()
            tree = ast.parse(content)
            self.finished_signal.emit(True, "语法检查通过", None)
        except SyntaxError as e:
            self.log_signal.emit(f"语法错误: {e}")
            fix_info = self._analyze_syntax_error(content, e)
            self.finished_signal.emit(False, str(e), fix_info)
        except Exception as e:
            self.finished_signal.emit(False, str(e), None)

    def _analyze_syntax_error(self, content, error):
        lines = content.split('\n')
        lineno = error.lineno - 1 if error.lineno else 0
        context_start = max(0, lineno - 3)
        context_end = min(len(lines), lineno + 3)
        context = []
        changes = []
        for i in range(context_start, context_end):
            prefix = ">>>" if i == lineno else "   "
            context.append(f"{prefix} {i+1:4d} | {lines[i].rstrip()}")
        if lineno < len(lines):
            line = lines[lineno]
            if line.rstrip().endswith(':') and lineno + 1 < len(lines):
                next_line = lines[lineno + 1]
                if next_line.strip() and not next_line.startswith((' ', '\t')):
                    changes.append(f"{lineno+1}: ")
        return {'context': context, 'changes': changes, 'lineno': lineno + 1, 'error_msg': str(error)}

class GitHubFetchWorker(QThread):
    log_signal = pyqtSignal(str)
    finished_signal = pyqtSignal(bool, str, str)

    def __init__(self, url):
        super().__init__()
        self.url = url

    def run(self):
        try:
            import ssl
            match = re.search(r'github\.com/([^/]+)/([^/]+?)(?:/tree/([^/]+))?', self.url)
            if not match:
                raw_url = self.url.replace('github.com', 'raw.githubusercontent.com').replace('/blob/', '/')
                self.log_signal.emit(f"Downloading: {raw_url}")
                context = ssl.create_default_context()
                context.check_hostname = False
                context.verify_mode = ssl.CERT_NONE
                req = urllib.request.Request(raw_url, headers={'User-Agent': 'Mozilla/5.0'})
                with urllib.request.urlopen(req, context=context, timeout=30) as resp:
                    content = resp.read().decode('utf-8', errors='replace')
                filename = os.path.basename(raw_url)
                save_path = os.path.join(os.getcwd(), filename)
                with open(save_path, 'w', encoding='utf-8') as f:
                    f.write(content)
                self.finished_signal.emit(True, f"Saved: {save_path}", save_path)
                return
            owner = match.group(1)
            repo = match.group(2)
            branch = match.group(3) or 'main'
            api_url = f"https://api.github.com/repos/{owner}/{repo}/git/trees/{branch}?recursive=1"
            self.log_signal.emit(f"Fetching repo tree: {owner}/{repo}")
            context = ssl.create_default_context()
            context.check_hostname = False
            context.verify_mode = ssl.CERT_NONE
            req = urllib.request.Request(api_url, headers={
                'User-Agent': 'Mozilla/5.0',
                'Accept': 'application/vnd.github.v3+json'
            })
            with urllib.request.urlopen(req, context=context, timeout=15) as resp:
                data = json.loads(resp.read().decode('utf-8'))
            py_files = [item for item in data.get('tree', []) if item['path'].endswith('.py')]
            if not py_files:
                self.finished_signal.emit(False, "No .py files found in repo", "")
                return
            if len(py_files) == 1:
                selected = py_files[0]
            else:
                main_candidates = [f for f in py_files if f['path'].lower() in ('main.py', 'app.py', 'run.py', 'index.py')]
                selected = main_candidates[0] if main_candidates else py_files[0]
            raw_url = f"https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{selected['path']}"
            self.log_signal.emit(f"Downloading: {selected['path']}")
            context = ssl.create_default_context()
            context.check_hostname = False
            context.verify_mode = ssl.CERT_NONE
            req = urllib.request.Request(raw_url, headers={'User-Agent': 'Mozilla/5.0'})
            with urllib.request.urlopen(req, context=context, timeout=30) as resp:
                content = resp.read().decode('utf-8', errors='replace')
            save_path = os.path.join(os.getcwd(), selected['path'].replace('/', '_'))
            with open(save_path, 'w', encoding='utf-8') as f:
                f.write(content)
            self.finished_signal.emit(True, f"Saved: {save_path}", save_path)
        except Exception as e:
            self.finished_signal.emit(False, str(e), "")

# ==================== MainWindow ====================
class MainWindow(QMainWindow):
    """Python脚本打包工具 - 主窗口"""

    def __init__(self):
        super().__init__()
        self.pack_thread = None
        self.monitor_thread = None
        self.progress_bar = None
        self._python_path = None
        self._data_files = []
        self._pack_scripts = []
        self.setWindowTitle(f"Python 代码跨平台打包工具 v{VERSION}")
        self.setMinimumSize(1100, 780)
        self._setup_ui()
        self._center_on_screen()
        self._start_monitor()
        self._auto_detect_python()
        self._auto_detect_upx()
        QTimer.singleShot(300, self._enable_drag_drop)

    def _center_on_screen(self):
        screen = QApplication.primaryScreen().availableGeometry()
        w = min(1200, int(screen.width() * 0.85))
        h = min(850, int(screen.height() * 0.85))
        self.resize(w, h)
        x = screen.x() + (screen.width() - w) // 2
        y = screen.y() + (screen.height() - h) // 2
        self.move(x, y)

    def _enable_drag_drop(self):
        if hasattr(self, 'script_edit'):
            self.script_edit.enable_drag_drop()

    def _setup_ui(self):
        central = QWidget()
        self.setCentralWidget(central)
        main_layout = QVBoxLayout(central)
        main_layout.setContentsMargins(12, 8, 12, 8)
        main_layout.setSpacing(6)

        # Toolbar
        toolbar = QWidget()
        tb = QHBoxLayout(toolbar)
        tb.setContentsMargins(0, 0, 0, 0)
        tb.setSpacing(6)

        self.packer_combo = QComboBox()
        self.packer_combo.addItems([
            'PyInstaller-cmd', 'PyInstaller-spec', 'Nuitka',
            'Py2exe', 'Cx_Freeze', 'PyApp', 'Pynsist', 'PyOxidizer', 'Py2app'
        ])
        self.packer_combo.setFixedWidth(140)
        self.packer_combo.currentTextChanged.connect(self._on_packer_changed)
        tb.addWidget(QLabel("📦 打包器:"))
        tb.addWidget(self.packer_combo)
        tb.addSpacing(8)

        self.script_edit = DragDropLineEdit()
        self.script_edit.setPlaceholderText("选择或拖拽 .py 脚本文件到此处...")
        self.script_edit.setMinimumHeight(28)
        self.script_edit.textChanged.connect(self._on_script_changed)
        tb.addWidget(self.script_edit, stretch=1)

        browse_btn = QPushButton("📂 浏览")
        browse_btn.setFixedWidth(65)
        browse_btn.clicked.connect(self._browse_script)
        tb.addWidget(browse_btn)

        github_btn = QPushButton("🌐 GitHub")
        github_btn.setFixedWidth(75)
        github_btn.setToolTip("从GitHub获取源码")
        github_btn.clicked.connect(self._fetch_github)
        tb.addWidget(github_btn)

        main_layout.addWidget(toolbar)

        # Content splitter
        content_splitter = QSplitter(Qt.Orientation.Horizontal)
        content_splitter.setHandleWidth(4)

        # Left: config
        left_scroll = QScrollArea()
        left_scroll.setWidgetResizable(True)
        left_scroll.setFrameShape(QFrame.Shape.NoFrame)
        left_widget = QWidget()
        left_layout = QVBoxLayout(left_widget)
        left_layout.setContentsMargins(0, 0, 6, 0)
        left_layout.setSpacing(4)

        basic_box = CollapsibleBox("🔧 基础设置")
        self._setup_basic_options(basic_box)
        left_layout.addWidget(basic_box)

        adv_box = CollapsibleBox("⚙ 高级选项")
        self._setup_advanced_options(adv_box)
        left_layout.addWidget(adv_box)

        self.nuitka_box = CollapsibleBox("🔬 Nuitka 专用选项")
        self._setup_nuitka_options(self.nuitka_box)
        left_layout.addWidget(self.nuitka_box)
        self.nuitka_box.setVisible(False)

        ver_box = CollapsibleBox("📋 版本信息")
        self._setup_version_options(ver_box)
        left_layout.addWidget(ver_box)

        left_layout.addStretch()
        left_scroll.setWidget(left_widget)
        content_splitter.addWidget(left_scroll)

        # Right: log + progress
        right_widget = QWidget()
        right_layout = QVBoxLayout(right_widget)
        right_layout.setContentsMargins(6, 0, 0, 0)
        right_layout.setSpacing(4)

        # Progress
        progress_frame = QWidget()
        progress_layout = QVBoxLayout(progress_frame)
        progress_layout.setContentsMargins(0, 0, 0, 0)
        progress_layout.setSpacing(2)

        progress_header = QHBoxLayout()
        progress_header.addWidget(QLabel("📊 打包进度:"))

        self.progress_style_combo = QComboBox()
        self.progress_style_combo.addItems([
            '🎨 彩色条纹', '⚡ 简洁', '🌊 波浪', '🔵 点阵',
            '☘️ 薄荷绿', '🌸 樱花粉', '🪐 星际紫', '🌊 深海蓝'
        ])
        self.progress_style_combo.setFixedWidth(100)
        self.progress_style_combo.currentTextChanged.connect(self._switch_progress_style)
        progress_header.addWidget(self.progress_style_combo)
        progress_header.addStretch()
        progress_layout.addLayout(progress_header)

        self.progress_container = QStackedWidget()
        self.progress_container.setFixedHeight(55)
        self._init_progress_bars()
        progress_layout.addWidget(self.progress_container)
        progress_frame.setLayout(progress_layout)
        right_layout.addWidget(progress_frame)

        # Buttons
        btn_row = QHBoxLayout()
        btn_row.setSpacing(8)

        self.check_btn = QPushButton("🔍 语法检查")
        self.check_btn.setStyleSheet("background:#17a2b8; color:white; font-weight:bold; padding:6px 14px; border-radius:4px;")
        self.check_btn.clicked.connect(self._run_syntax_check)
        btn_row.addWidget(self.check_btn)

        self.estimate_btn = QPushButton("📏 大小预估")
        self.estimate_btn.setStyleSheet("background:#6c757d; color:white; padding:6px 12px; border-radius:4px;")
        self.estimate_btn.clicked.connect(self._estimate_size)
        btn_row.addWidget(self.estimate_btn)

        btn_row.addStretch()

        self.pack_btn = QPushButton("🚀 开始打包")
        self.pack_btn.setStyleSheet("background:#4CAF50; color:white; font-weight:bold; padding:8px 24px; border-radius:6px; font-size:13px;")
        self.pack_btn.clicked.connect(self._start_pack)
        btn_row.addWidget(self.pack_btn)

        self.stop_btn = QPushButton("⏹ 停止")
        self.stop_btn.setEnabled(False)
        self.stop_btn.setStyleSheet("background:#f44336; color:white; font-weight:bold; padding:8px 20px; border-radius:6px;")
        self.stop_btn.clicked.connect(self._stop_pack)
        btn_row.addWidget(self.stop_btn)

        self.about_btn = QPushButton("ℹ️ 关于")
        self.about_btn.setStyleSheet("background:#2196F3; color:white; padding:8px 16px; border-radius:6px;")
        self.about_btn.clicked.connect(self._show_about)
        btn_row.addWidget(self.about_btn)
        right_layout.addLayout(btn_row)

        # Log area
        right_layout.addWidget(QLabel("📝 打包日志:"))
        self.log_edit = LogTextEdit()
        self.log_edit.setMinimumHeight(200)
        self.log_edit.files_dropped.connect(self._on_data_files_dropped)
        right_layout.addWidget(self.log_edit, stretch=1)

        # 系统资源监控栏
        monitor_frame = QWidget()
        monitor_frame.setStyleSheet("background:#f5f5f5; border-radius:6px; padding:4px;")
        monitor_layout = QHBoxLayout(monitor_frame)
        monitor_layout.setContentsMargins(8, 2, 8, 2)
        monitor_layout.setSpacing(16)

        self.cpu_label = QLabel("💻 CPU: --")
        self.cpu_label.setStyleSheet("font-weight:bold; color:#333;")
        monitor_layout.addWidget(self.cpu_label)

        self.mem_label = QLabel("🧠 内存: --")
        self.mem_label.setStyleSheet("font-weight:bold; color:#333;")
        monitor_layout.addWidget(self.mem_label)

        self.temp_label = QLabel("🌡 温度: --")
        self.temp_label.setStyleSheet("font-weight:bold; color:#333;")
        monitor_layout.addWidget(self.temp_label)

        monitor_layout.addStretch()
        self.time_label = QLabel("")
        self.time_label.setStyleSheet("color:#888; font-size:10px;")
        monitor_layout.addWidget(self.time_label)
        right_layout.addWidget(monitor_frame)

        content_splitter.addWidget(right_widget)
        content_splitter.setSizes([420, 680])
        main_layout.addWidget(content_splitter, stretch=1)

        self.status_label = QLabel("就绪")
        self.status_label.setStyleSheet("padding:2px 8px; color:#666;")
        main_layout.addWidget(self.status_label)

    def _setup_basic_options(self, box):
        grid = QGridLayout()
        grid.setSpacing(6)

        # 输出目录
        grid.addWidget(QLabel("输出目录:"), 0, 0)
        self.output_edit = QLineEdit()
        self.output_edit.setPlaceholderText("默认: 脚本所在目录/dist")
        grid.addWidget(self.output_edit, 0, 1)
        output_btn = QPushButton("📂")
        output_btn.setFixedWidth(35)
        output_btn.clicked.connect(lambda: self._browse_dir(self.output_edit))
        grid.addWidget(output_btn, 0, 2)

        # 程序名称
        grid.addWidget(QLabel("程序名称:"), 1, 0)
        self.name_edit = QLineEdit()
        self.name_edit.setPlaceholderText("默认: 脚本文件名")
        grid.addWidget(self.name_edit, 1, 1, 1, 2)

        # 图标
        grid.addWidget(QLabel("图标(.ico):"), 2, 0)
        self.icon_edit = QLineEdit()
        self.icon_edit.setPlaceholderText("选择 .ico 图标文件")
        grid.addWidget(self.icon_edit, 2, 1)
        icon_btn = QPushButton("📂")
        icon_btn.setFixedWidth(35)
        icon_btn.clicked.connect(lambda: self._browse_file(self.icon_edit, "图标文件 (*.ico *.png)"))
        grid.addWidget(icon_btn, 2, 2)

        # 打包模式
        grid.addWidget(QLabel("打包模式:"), 3, 0)
        mode_widget = QWidget()
        mode_layout = QHBoxLayout(mode_widget)
        mode_layout.setContentsMargins(0, 0, 0, 0)
        self.onefile_cb = QCheckBox("单文件")
        self.onefile_cb.setChecked(True)
        self.onefile_cb.setToolTip("打包为单个exe文件(推荐)")
        mode_layout.addWidget(self.onefile_cb)
        self.console_cb = QCheckBox("控制台")
        self.console_cb.setToolTip("显示控制台窗口(调试用)")
        mode_layout.addWidget(self.console_cb)
        self.clean_cb = QCheckBox("清理")
        self.clean_cb.setToolTip("打包前清理临时文件")
        mode_layout.addWidget(self.clean_cb)
        self.strip_cb = QCheckBox("去符号")
        self.strip_cb.setChecked(True)
        self.strip_cb.setToolTip("去除调试符号,减小体积")
        mode_layout.addWidget(self.strip_cb)
        mode_layout.addStretch()
        grid.addWidget(mode_widget, 3, 1, 1, 2)

        # UPX压缩
        grid.addWidget(QLabel("UPX压缩:"), 4, 0)
        compress_widget = QWidget()
        compress_layout = QHBoxLayout(compress_widget)
        compress_layout.setContentsMargins(0, 0, 0, 0)
        self.compress_combo = QComboBox()
        self.compress_combo.addItems(['不压', '最快', '默认', '最好', '极致'])
        self.compress_combo.setCurrentText('默认')
        self.compress_combo.setToolTip("UPX压缩级别,级别越高压缩率越大但越慢")
        compress_layout.addWidget(self.compress_combo)
        self.upx_edit = QLineEdit()
        self.upx_edit.setPlaceholderText("UPX路径(自动检测)")
        self.upx_edit.setReadOnly(True)
        compress_layout.addWidget(self.upx_edit, stretch=1)
        grid.addWidget(compress_widget, 4, 1, 1, 2)

        # Python路径
        grid.addWidget(QLabel("Python:"), 5, 0)
        self.python_edit = QLineEdit()
        self.python_edit.setReadOnly(True)
        self.python_edit.setPlaceholderText("自动检测中...")
        grid.addWidget(self.python_edit, 5, 1, 1, 2)

        # 虚拟环境
        self.venv_cb = QCheckBox("使用虚拟环境")
        self.venv_cb.setToolTip("在虚拟环境中打包,隔离系统环境")
        grid.addWidget(self.venv_cb, 6, 0)
        self.venv_edit = QLineEdit()
        self.venv_edit.setPlaceholderText("虚拟环境路径(自动检测)")
        self.venv_edit.setReadOnly(True)
        grid.addWidget(self.venv_edit, 6, 1)
        detect_venv_btn = QPushButton("🔍 检测")
        detect_venv_btn.setFixedWidth(55)
        detect_venv_btn.setToolTip("自动检测虚拟环境")
        detect_venv_btn.clicked.connect(self._detect_venv)
        grid.addWidget(detect_venv_btn, 6, 2)

        box.add_layout(grid)

    def _setup_advanced_options(self, box):
        layout = QVBoxLayout()
        layout.setSpacing(4)

        # 隐藏导入
        hi_layout = QHBoxLayout()
        hi_layout.addWidget(QLabel("隐藏导入:"))
        self.hidden_import_edit = QLineEdit()
        self.hidden_import_edit.setPlaceholderText("逗号分隔,如: torch,numpy,pandas")
        hi_layout.addWidget(self.hidden_import_edit, stretch=1)
        layout.addLayout(hi_layout)

        # 排除模块
        ex_layout = QHBoxLayout()
        ex_layout.addWidget(QLabel("排除模块:"))
        self.exclude_edit = QLineEdit()
        self.exclude_edit.setPlaceholderText("逗号分隔,如: tkinter,test,unittest")
        ex_layout.addWidget(self.exclude_edit, stretch=1)
        layout.addLayout(ex_layout)

        # 数据文件
        data_header = QHBoxLayout()
        data_header.addWidget(QLabel("📎 数据文件:"))
        add_data_btn = QPushButton("+ 添加")
        add_data_btn.setFixedWidth(55)
        add_data_btn.setToolTip("添加程序需要的额外文件")
        add_data_btn.clicked.connect(self._add_data_file)
        data_header.addWidget(add_data_btn)
        data_header.addStretch()
        layout.addLayout(data_header)

        self.data_list = QListWidget()
        self.data_list.setMaximumHeight(60)
        self.data_list.setStyleSheet("QListWidget { font-size:10px; }")
        layout.addWidget(self.data_list)

        # 外部脚本
        script_header = QHBoxLayout()
        script_header.addWidget(QLabel("📜 外部脚本:"))
        add_script_btn = QPushButton("+ 添加")
        add_script_btn.setFixedWidth(55)
        add_script_btn.setToolTip("添加需要一起打包的外部脚本")
        add_script_btn.clicked.connect(self._add_pack_script)
        script_header.addWidget(add_script_btn)
        script_header.addStretch()
        layout.addLayout(script_header)

        self.script_list = QListWidget()
        self.script_list.setMaximumHeight(60)
        self.script_list.setStyleSheet("QListWidget { font-size:10px; }")
        layout.addWidget(self.script_list)

        # 额外参数
        extra_layout = QHBoxLayout()
        extra_layout.addWidget(QLabel("额外参数:"))
        self.extra_args_edit = QLineEdit()
        self.extra_args_edit.setPlaceholderText("直接传递给打包器的额外参数")
        extra_layout.addWidget(self.extra_args_edit, stretch=1)
        layout.addLayout(extra_layout)

        # 日志级别
        log_layout = QHBoxLayout()
        log_layout.addWidget(QLabel("日志级别:"))
        self.log_level_combo = QComboBox()
        self.log_level_combo.addItems(['INFO', 'DEBUG', 'WARN', 'ERROR', 'CRITICAL'])
        log_layout.addWidget(self.log_level_combo)
        log_layout.addStretch()
        layout.addLayout(log_layout)

        box.add_layout(layout)

    def _setup_nuitka_options(self, box):
        grid = QGridLayout()
        grid.setSpacing(6)

        grid.addWidget(QLabel("编译器:"), 0, 0)
        self.backend_combo = QComboBox()
        self.backend_combo.addItems(['auto', 'MinGW64', 'MSVC'])
        self.backend_combo.setToolTip("选择C编译器后端")
        grid.addWidget(self.backend_combo, 0, 1)

        grid.addWidget(QLabel("并行数:"), 1, 0)
        self.jobs_combo = QComboBox()
        self.jobs_combo.addItems(['auto'] + [str(i) for i in range(1, 17)])
        self.jobs_combo.setToolTip("并行编译线程数")
        grid.addWidget(self.jobs_combo, 1, 1)

        grid.addWidget(QLabel("LTO:"), 2, 0)
        self.lto_combo = QComboBox()
        self.lto_combo.addItems(['no', 'yes', 'thin'])
        self.lto_combo.setToolTip("链接时优化,可能增大编译时间和内存占用")
        grid.addWidget(self.lto_combo, 2, 1)

        grid.addWidget(QLabel("优化级别:"), 3, 0)
        self.optimize_combo = QComboBox()
        self.optimize_combo.addItems(['平衡', '速度优先', '体积优先'])
        grid.addWidget(self.optimize_combo, 3, 1)

        self.nuitka_compat_cb = QCheckBox("兼容模式")
        self.nuitka_compat_cb.setToolTip("使用Nuitka旧版兼容参数")
        grid.addWidget(self.nuitka_compat_cb, 4, 0)

        self.low_mem_cb = QCheckBox("低内存模式")
        self.low_mem_cb.setToolTip("减少并行编译,降低内存占用")
        grid.addWidget(self.low_mem_cb, 4, 1)

        self.no_ccache_cb = QCheckBox("禁用ccache")
        self.no_ccache_cb.setToolTip("禁用编译缓存加速")
        grid.addWidget(self.no_ccache_cb, 5, 0)

        self.experimental_cb = QCheckBox("实验性功能")
        self.experimental_cb.setToolTip("启用Nuitka实验性功能")
        grid.addWidget(self.experimental_cb, 5, 1)

        box.add_layout(grid)

    def _setup_version_options(self, box):
        grid = QGridLayout()
        grid.setSpacing(6)

        grid.addWidget(QLabel("产品名称:"), 0, 0)
        self.product_name_edit = QLineEdit()
        self.product_name_edit.setPlaceholderText("如: MyApp")
        grid.addWidget(self.product_name_edit, 0, 1)

        grid.addWidget(QLabel("公司名称:"), 1, 0)
        self.company_edit = QLineEdit()
        self.company_edit.setPlaceholderText("如: MyCompany")
        grid.addWidget(self.company_edit, 1, 1)

        grid.addWidget(QLabel("文件版本:"), 2, 0)
        self.file_ver_edit = QLineEdit()
        self.file_ver_edit.setPlaceholderText("如: 1.0.0.0")
        grid.addWidget(self.file_ver_edit, 2, 1)

        grid.addWidget(QLabel("产品版本:"), 3, 0)
        self.product_ver_edit = QLineEdit()
        self.product_ver_edit.setPlaceholderText("如: 1.0.0")
        grid.addWidget(self.product_ver_edit, 3, 1)

        box.add_layout(grid)

    def _init_progress_bars(self):
        self.all_progress_bars = {
            '🎨 彩色条纹': StripedProgressBar(),
            '⚡ 简洁': EmojiProgressBar(),
            '🌊 波浪': WaveProgressBar(),
            '🔵 点阵': DotProgressBar(),
            '☘️ 薄荷绿': GreenProgressBar(),
            '🌸 樱花粉': PinkProgressBar(),
            '🪐 星际紫': PurpleProgressBar(),
            '🌊 深海蓝': BlueProgressBar(),
        }
        for name, bar in self.all_progress_bars.items():
            self.progress_container.addWidget(bar)
        self.progress_bar = self.all_progress_bars['🎨 彩色条纹']
        self.progress_container.setCurrentWidget(self.progress_bar)

    def _switch_progress_style(self, style_name):
        if style_name in self.all_progress_bars:
            new_bar = self.all_progress_bars[style_name]
            try:
                cur_val = self.progress_bar.value()
            except:
                cur_val = getattr(self.progress_bar, '_value', 0)
            new_bar.setValue(cur_val)
            self.progress_bar = new_bar
            self.progress_container.setCurrentWidget(new_bar)

    # ===== Slots =====
    def _on_packer_changed(self, packer):
        self.nuitka_box.setVisible(packer == 'Nuitka')

    def _on_script_changed(self, path):
        if path and os.path.exists(path):
            self.status_label.setText(f"📄 脚本: {os.path.basename(path)}")
            basename = os.path.splitext(os.path.basename(path))[0]
            if not self.name_edit.text():
                self.name_edit.setPlaceholderText(basename)
            parent_dir = os.path.dirname(os.path.abspath(path))
            if not self.output_edit.text():
                self.output_edit.setPlaceholderText(os.path.join(parent_dir, 'dist'))

    def _browse_script(self):
        path, _ = QFileDialog.getOpenFileName(self, "选择Python脚本", "", "Python文件 (*.py *.pyw *.spec);;所有文件 (*)")
        if path:
            self.script_edit.setText(os.path.normpath(path))

    def _browse_dir(self, edit):
        path = QFileDialog.getExistingDirectory(self, "选择目录")
        if path:
            edit.setText(os.path.normpath(path))

    def _browse_file(self, edit, filter_str):
        path, _ = QFileDialog.getOpenFileName(self, "选择文件", "", filter_str)
        if path:
            edit.setText(os.path.normpath(path))

    def _add_data_file(self):
        files, _ = QFileDialog.getOpenFileNames(self, "选择数据文件", "", "所有文件 (*)")
        for f in files:
            norm = os.path.normpath(f)
            if norm not in self._data_files:
                self._data_files.append(norm)
                self.data_list.addItem(f"{os.path.basename(norm)} → .")

    def _add_pack_script(self):
        files, _ = QFileDialog.getOpenFileNames(self, "选择外部脚本", "", "Python文件 (*.py)")
        for f in files:
            norm = os.path.normpath(f)
            if norm not in self._pack_scripts:
                self._pack_scripts.append(norm)
                self.script_list.addItem(os.path.basename(norm))

    def _on_data_files_dropped(self, files):
        for f in files:
            norm = os.path.normpath(f)
            if norm not in self._data_files:
                self._data_files.append(norm)
                self.data_list.addItem(f"{os.path.basename(norm)} -> .")

    def _fetch_github(self):
        url, ok = QInputDialog.getText(self, "GitHub 源码", "输入 GitHub 仓库 URL 或 raw 文件 URL:")
        if ok and url.strip():
            self.github_worker = GitHubFetchWorker(url.strip())
            self.github_worker.log_signal.connect(self.safe_log)
            self.github_worker.finished_signal.connect(self._on_github_fetched)
            self.github_worker.start()
            self.safe_log(f"🌐 正在从 GitHub 获取: {url.strip()}")

    def _on_github_fetched(self, success, msg, save_path):
        if success and save_path:
            self.script_edit.setText(save_path)
        self.safe_log(msg)

    def _run_syntax_check(self):
        script = self._get_script()
        if not script:
            show_msg(self, "提示", "请先选择 Python 脚本", 2)
            return
        self.safe_log("🔍 开始语法检查...")
        self.check_worker = SyntaxCheckWorker(script)
        self.check_worker.log_signal.connect(self.safe_log)
        self.check_worker.finished_signal.connect(self._on_syntax_checked)
        self.check_worker.start()

    def _on_syntax_checked(self, success, msg, fix_info):
        self.safe_log(msg)
        if not success and fix_info:
            ctx = fix_info.get('context', [])
            changes = fix_info.get('changes', [])
            if ctx:
                self.safe_log("--- 错误上下文 ---")
                for line in ctx:
                    self.safe_log(line)
            if changes:
                self.safe_log("--- 修复建议 ---")
                for c in changes:
                    self.safe_log(f"💡 {c}")
                self._offer_auto_fix(fix_info)

    def _offer_auto_fix(self, fix_info):
        script = self._get_script()
        if not script:
            return
        reply = QMessageBox.question(self, "语法修复",
            "检测到语法错误,是否尝试自动修复?\n\n将创建备份文件 (.bak.py)。",
            QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
        if reply == QMessageBox.StandardButton.Yes:
            self._auto_fix_syntax(script, fix_info)

    def _auto_fix_syntax(self, script_path, fix_info):
        try:
            with open(script_path, 'r', encoding='utf-8') as f:
                content = f.read()
            backup = script_path + '.bak.py'
            shutil.copy2(script_path, backup)
            self.safe_log(f"📁 备份已保存: {os.path.basename(backup)}")
            lines = content.split('\n')
            lineno = fix_info.get('lineno', 1) - 1
            changes_made = []
            if lineno < len(lines):
                line = lines[lineno]
                new_line = line
                if ':' in new_line:
                    new_line = new_line.replace(':', ':')
                    if new_line != line:
                        changes_made.append("中文冒号 → 英文冒号")
                lines[lineno] = new_line
                if new_line.rstrip().endswith(':') and lineno + 1 < len(lines):
                    next_line = lines[lineno + 1]
                    if next_line.strip() and not next_line.startswith((' ', '\t')):
                        lines[lineno + 1] = '    ' + next_line.lstrip()
                        changes_made.append(f"第{lineno+2}行:添加缩进")
            new_content = '\n'.join(lines)
            changes_desc = [f"第{lineno+1}行: {c}" for c in changes_made]
            dlg = CodePreviewDialog(self, content, new_content, changes_desc, script_path, backup)
            if dlg.exec() == QDialog.DialogCode.Accepted:
                with open(script_path, 'w', encoding='utf-8') as f:
                    f.write(new_content)
                self.safe_log("✅ 语法修复已应用")
            else:
                self.safe_log("↩️ 已取消修复,从备份还原")
                if os.path.exists(backup):
                    shutil.copy2(backup, script_path)
        except Exception as e:
            self.safe_log(f"❌ 自动修复失败: {e}")

    def _estimate_size(self):
        script = self._get_script()
        if not script:
            show_msg(self, "提示", "请先选择 Python 脚本", 2)
            return
        self.safe_log("📏 正在分析脚本大小预估...")
        try:
            script_size = os.path.getsize(script)
            self.safe_log(f"  源文件: {script_size / 1024:.1f} KB")
            with open(script, 'r', encoding='utf-8') as f:
                content = f.read()
            imports = set()
            for match in re.finditer(r'^(?:import\s+([\w.]+)|from\s+([\w.]+)\s+import)', content, re.MULTILINE):
                mod = match.group(1) or match.group(2)
                if mod:
                    imports.add(mod.split('.')[0])
            user_imports = [m for m in imports if m not in STANDARD_LIBS]
            self.safe_log(f"  标准库导入: {len(imports) - len(user_imports)} 个")
            self.safe_log(f"  第三方导入: {len(user_imports)} 个")
            if user_imports:
                self.safe_log(f"  第三方包: {', '.join(sorted(user_imports))}")
            est_base = 8
            est_deps = len(user_imports) * 15
            est_total = est_base + est_deps
            self.safe_log(f"  📏 预估体积: {est_base}-{est_total} MB")
            self.safe_log(f"  💡 提示: UPX 压缩可减小 30%-50%,排除无用模块可进一步优化")
        except Exception as e:
            self.safe_log(f"❌ 预估失败: {e}")

    def _start_pack(self):
        script = self._get_script()
        if not script:
            show_msg(self, "提示", "请先选择 Python 脚本", 2)
            return
        if self.pack_thread and self.pack_thread.isRunning():
            show_msg(self, "提示", "正在打包中,请稍候...", 2)
            return

        # 预检:找到真正的 Python 解释器(exe 自身不是 Python)
        real_python = _get_real_python()
        if not real_python:
            show_msg(self, "未找到 Python",
                "当前环境未检测到系统 Python 解释器。\n\n"
                "打包功能需要系统 Python 来调用打包器。\n"
                "请安装 Python 3.9+ 后重试。", 5)
            return

        # 预检:确保打包器已安装
        packer = self.packer_combo.currentText()
        packer_info = {
            'PyInstaller-cmd': ('pyinstaller', 'PyInstaller'),
            'PyInstaller-spec': ('pyinstaller', 'PyInstaller'),
            'Nuitka': ('nuitka', 'nuitka'),
            'Py2exe': ('py2exe', 'py2exe'),
            'Cx_Freeze': ('cx-Freeze', 'cx_Freeze'),
            'PyApp': ('pyapp', 'pyapp'),
            'Pynsist': ('pynsist', 'pynsist'),
            'PyOxidizer': ('pyoxidizer', 'pyoxidizer'),
            'Py2app': ('py2app', 'py2app'),
        }
        pip_name, import_name = packer_info.get(packer, ('pyinstaller', 'PyInstaller'))
        try:
            check = subprocess.run(
                [real_python, '-c', f'import {import_name}'],
                capture_output=True, text=True, timeout=5,
                startupinfo=get_startupinfo()
            )
            if check.returncode != 0:
                reply = QMessageBox.question(self, "缺少打包器",
                    f"未检测到 {packer}!\n\n"
                    f"请先安装: pip install {pip_name}\n\n"
                    f"是否现在安装?",
                    QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
                if reply == QMessageBox.StandardButton.Yes:
                    self.safe_log(f"📥 正在安装 {pip_name}...")
                    install_result = subprocess.run(
                        [real_python, '-m', 'pip', 'install', pip_name, '-i', MIRROR],
                        capture_output=True, text=True, timeout=120,
                        startupinfo=get_startupinfo()
                    )
                    if install_result.returncode == 0:
                        self.safe_log(f"✅ {pip_name} 安装成功")
                    else:
                        self.safe_log(f"❌ 安装失败: {install_result.stderr[:200]}")
                        show_msg(self, "安装失败", f"请手动安装: pip install {pip_name}", 3)
                        return
                else:
                    return
        except Exception as e:
            self.safe_log(f"⚠️ 无法检测打包器: {e}")

        config = self._build_config()
        self.pack_thread = PackageWorker(config)
        self.pack_thread.log_signal.connect(self.safe_log)
        self.pack_thread.progress_signal.connect(self._on_progress)
        self.pack_thread.finished_signal.connect(self._on_pack_finished)
        self.pack_thread.start()
        self.pack_btn.setEnabled(False)
        self.stop_btn.setEnabled(True)
        self.status_label.setText("🚀 正在打包...")

    def _stop_pack(self):
        if self.pack_thread and self.pack_thread.isRunning():
            self.pack_thread.stop()
            self.safe_log("⏹ 用户停止打包")
            self.stop_btn.setEnabled(False)

    def _on_progress(self, value):
        self.progress_bar.setValue(value)

    def _on_pack_finished(self, success, msg):
        self.pack_btn.setEnabled(True)
        self.stop_btn.setEnabled(False)
        if success:
            self.progress_bar.setValue(100)
            self.status_label.setText("✅ 打包完成!")
            self.safe_log(f"✅ {msg}")
            script = self._get_script()
            if script:
                output_dir = self.output_edit.text() or os.path.join(os.path.dirname(script), 'dist')
                if os.path.exists(output_dir):
                    reply = QMessageBox.question(self, "打包完成",
                        f"打包成功!\n\n是否打开输出目录?\n{output_dir}",
                        QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
                    if reply == QMessageBox.StandardButton.Yes:
                        if sys.platform == 'win32':
                            os.startfile(output_dir)
                        else:
                            webbrowser.open(output_dir)
        else:
            self.status_label.setText(f"❌ 打包失败: {msg}")
            self.safe_log(f"❌ {msg}")

    def _show_about(self):
        dlg = AboutDialog(self)
        dlg.exec()

    # ===== 自动检测 =====
    def _auto_detect_python(self):
        real_python = _get_real_python()
        if real_python:
            self._python_path = real_python
            self.python_edit.setText(real_python)
            try:
                result = subprocess.run([real_python, '--version'], capture_output=True, text=True, timeout=3,
                                        startupinfo=get_startupinfo())
                if result.returncode == 0:
                    ver = (result.stdout or result.stderr).strip()
                    self.python_edit.setToolTip(ver)
            except:
                pass
        else:
            self.python_edit.setText("⚠️ 未检测到 Python")

    def _auto_detect_upx(self):
        upx_exe = 'upx.exe' if sys.platform == 'win32' else 'upx'
        upx_path = shutil.which(upx_exe)
        if not upx_path:
            exe_dir = get_exe_directory()
            local_upx = os.path.join(exe_dir, upx_exe)
            if os.path.exists(local_upx):
                upx_path = local_upx
        if upx_path:
            self.upx_edit.setText(upx_path)
            self.upx_edit.setStyleSheet("color: green;")
        else:
            self.upx_edit.setText("未检测到 UPX")

    def _detect_venv(self):
        script = self._get_script()
        search_start = os.path.dirname(script) if script else os.getcwd()
        venv_names = ['venv', '.venv', 'env', '.env', 'virtualenv']
        current = os.path.abspath(search_start)
        for _ in range(5):
            for name in venv_names:
                venv_dir = os.path.join(current, name)
                if os.path.isdir(venv_dir):
                    python_exe = 'Scripts/python.exe' if sys.platform == 'win32' else 'bin/python3'
                    python_path = os.path.join(venv_dir, python_exe)
                    if os.path.exists(python_path):
                        self.venv_edit.setText(venv_dir)
                        self.venv_cb.setChecked(True)
                        self.safe_log(f"🔍 检测到虚拟环境: {venv_dir}")
                        return
            parent = os.path.dirname(current)
            if parent == current:
                break
            current = parent
        self.safe_log("⚠️ 未检测到虚拟环境")

    def _start_monitor(self):
        self.monitor_thread = SystemMonitorThread()
        self.monitor_thread.status_updated.connect(self._on_monitor_update)
        self.monitor_thread.start()

    def _on_monitor_update(self, cpu, mem_percent, mem_used, mem_total, temp_str):
        cpu_color = "#F44336" if cpu > 80 else ("#FF9800" if cpu > 60 else "#4CAF50")
        mem_color = "#F44336" if mem_percent > 80 else ("#FF9800" if mem_percent > 60 else "#4CAF50")
        self.cpu_label.setText(f"💻 CPU: {cpu:.0f}%")
        self.cpu_label.setStyleSheet(f"font-weight:bold; color:{cpu_color};")
        self.mem_label.setText(f"🧠 内存: {mem_percent:.0f}% ({mem_used:.1f}/{mem_total:.0f}GB)")
        self.mem_label.setStyleSheet(f"font-weight:bold; color:{mem_color};")
        if temp_str:
            try:
                temp_val = float(temp_str.replace('C', '').replace('°', '').strip())
                temp_color = "#F44336" if temp_val > 80 else ("#FF9800" if temp_val > 65 else "#4CAF50")
            except:
                temp_color = "#4CAF50"
            self.temp_label.setText(f"🌡 温度: {temp_str}")
            self.temp_label.setStyleSheet(f"font-weight:bold; color:{temp_color};")
        else:
            self.temp_label.setText("🌡 温度: --")
        self.time_label.setText(datetime.datetime.now().strftime("%H:%M:%S"))

    # ===== Utilities =====
    def safe_log(self, msg):
        if msg is None:
            return
        try:
            timestamp = datetime.datetime.now().strftime("[%H:%M:%S]")
            self.log_edit.append_log(f"{timestamp} {msg}")
        except Exception:
            pass

    def _get_script(self):
        path = self.script_edit.text().strip()
        if path and os.path.isfile(path):
            return path
        return None

    def _build_config(self):
        script = self._get_script()
        venv_python = None
        venv_site_packages = None
        if self.venv_cb.isChecked() and self.venv_edit.text():
            venv_dir = self.venv_edit.text()
            if sys.platform == 'win32':
                venv_python = os.path.join(venv_dir, 'Scripts', 'python.exe')
            else:
                venv_python = os.path.join(venv_dir, 'bin', 'python3')
            lib_dir = os.path.join(venv_dir, 'Lib' if sys.platform == 'win32' else 'lib')
            for root, dirs, _ in os.walk(lib_dir):
                if 'site-packages' in dirs:
                    venv_site_packages = os.path.join(root, 'site-packages')
                    break

        hidden_imports = [m.strip() for m in self.hidden_import_edit.text().split(',') if m.strip()]
        excludes = [m.strip() for m in self.exclude_edit.text().split(',') if m.strip()]
        packer = self.packer_combo.currentText()
        config = {
            'packer': packer,
            'script': script,
            'target_python': self._python_path or sys.executable,
            'use_venv': self.venv_cb.isChecked() and bool(venv_python),
            'venv_python': venv_python,
            'venv_site_packages': venv_site_packages,
            'onefile': self.onefile_cb.isChecked(),
            'debug': self.console_cb.isChecked(),
            'clean': self.clean_cb.isChecked(),
            'strip': self.strip_cb.isChecked(),
            'name': self.name_edit.text().strip() or None,
            'output': self.output_edit.text().strip() or None,
            'icon': self.icon_edit.text().strip() or None,
            'hidden_imports': hidden_imports,
            'excludes': excludes,
            'data_files': [(f, '.') for f in self._data_files],
            'pack_scripts': self._pack_scripts[:],
            'extra_args': self.extra_args_edit.text().strip() or None,
            'compress_level': self.compress_combo.currentText(),
            'upx_path': self.upx_edit.text().strip() if os.path.exists(self.upx_edit.text().strip()) else '',
            'log_level': self.log_level_combo.currentText(),
            'platform': 'current',
            'collect': '',
            'copy_metadata': '',
            'backend': self.backend_combo.currentText(),
            'jobs': self.jobs_combo.currentText(),
            'lto': self.lto_combo.currentText(),
            'optimize': self.optimize_combo.currentText(),
            'nuitka_compat': self.nuitka_compat_cb.isChecked(),
            'low_memory': self.low_mem_cb.isChecked(),
            'disable_ccache': self.no_ccache_cb.isChecked(),
            'experimental': self.experimental_cb.isChecked(),
            'gui_plugin': 'auto',
            'has_mingw': shutil.which('gcc') is not None,
            'has_msvc': self._check_msvc(),
            'version_info': {
                'product_name': self.product_name_edit.text().strip() or None,
                'company': self.company_edit.text().strip() or None,
                'file_version': self.file_ver_edit.text().strip() or None,
                'product_version': self.product_ver_edit.text().strip() or None,
            },
            'version_file': None,
            'response_file': None,
            'cache_dir': get_exe_directory(),
        }
        return config

    def _check_msvc(self):
        try:
            result = subprocess.run(
                ['where', 'cl.exe'] if sys.platform == 'win32' else ['which', 'cl'],
                capture_output=True, text=True, timeout=5,
                startupinfo=get_startupinfo())
            return result.returncode == 0
        except:
            return False

    def closeEvent(self, event):
        if self.pack_thread and self.pack_thread.isRunning():
            reply = QMessageBox.question(self, "确认退出",
                "打包正在进行中,确定要退出吗?",
                QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
            if reply == QMessageBox.StandardButton.Yes:
                self.pack_thread.stop()
                self.pack_thread.wait(3000)
            else:
                event.ignore()
                return
        if self.monitor_thread:
            self.monitor_thread.stop()
        event.accept()

# ==================== 程序入口 ====================
def main():
    """主函数入口"""
    if hasattr(Qt, 'AA_EnableHighDpiScaling'):
        QApplication.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling, True)
    if hasattr(Qt, 'AA_UseHighDpiPixmaps'):
        QApplication.setAttribute(Qt.ApplicationAttribute.AA_UseHighDpiPixmaps, True)

    app = QApplication(sys.argv)
    app.setStyle('Fusion')

    palette = QPalette()
    palette.setColor(QPalette.ColorRole.Window, QColor(245, 245, 245))
    palette.setColor(QPalette.ColorRole.WindowText, QColor(33, 33, 33))
    palette.setColor(QPalette.ColorRole.Base, QColor(255, 255, 255))
    palette.setColor(QPalette.ColorRole.Text, QColor(33, 33, 33))
    palette.setColor(QPalette.ColorRole.Button, QColor(240, 240, 240))
    palette.setColor(QPalette.ColorRole.ButtonText, QColor(33, 33, 33))
    palette.setColor(QPalette.ColorRole.Highlight, QColor(33, 150, 243))
    palette.setColor(QPalette.ColorRole.HighlightedText, QColor(255, 255, 255))
    app.setPalette(palette)

    app.setStyleSheet("""
        QMainWindow { background-color: #f5f5f5; }
        QLineEdit { padding: 4px 8px; border: 1px solid #d0d0d0; border-radius: 4px; background-color: white; min-height: 22px; }
        QLineEdit:focus { border-color: #2196F3; }
        QComboBox { padding: 4px 8px; border: 1px solid #d0d0d0; border-radius: 4px; background-color: white; min-height: 22px; }
        QComboBox:focus { border-color: #2196F3; }
        QCheckBox { spacing: 4px; }
        QScrollArea { border: none; background: transparent; }
        QToolTip { background-color: #333; color: white; border: 1px solid #555; padding: 4px 8px; border-radius: 4px; font-size: 11px; }
    """)

    patch_subprocess_hide_window()
    ensure_python_on_startup()

    window = MainWindow()
    window.show()

    sys.exit(app.exec())

if __name__ == '__main__':
    main()

免费评分

参与人数 15吾爱币 +23 热心值 +14 收起 理由
Gogoing丶 + 1 热心回复!
WINGOING + 1 + 1 热心回复!
uuuuuuun + 1 + 1 谢谢@Thanks!
Eulogy + 1 + 1 谢谢@Thanks!
dhark + 1 + 1 谢谢@Thanks!
鱼鱼岛 + 1 + 1 我很赞同!
清炒藕片丶 + 1 + 1 热心回复!
fytz927 + 1 + 1 我很赞同!
蓦留 + 1 + 1 我很赞同!
chengdragon + 1 + 1 感谢分享
melove210 + 1 + 1 谢谢@Thanks!
overlord012 + 2 + 1 我很赞同!
lizhipei78 + 2 + 1 谢谢@Thanks!
love136 + 1 + 1 用心讨论,共获提升!
苏紫方璇 + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

发飙的熊猫君 发表于 2026-8-3 09:24
Traceback (most recent call last):
[09:25:23]   File "C:\Users\MZJ\AppData\Roaming\Python\Python313\site-packages\PyInstaller\building\utils.py", line 245, in process_collected_binary
[09:25:23]     p = subprocess.run(
[09:25:23]         cmd,
[09:25:23]     ...<5 lines>...
[09:25:23]         encoding='utf-8',
[09:25:23]     )
[09:25:23]   File "D:\Program Files\Python\Lib\subprocess.py", line 556, in run
[09:25:23]     with Popen(*popenargs, **kwargs) as process:
[09:25:23]          ~~~~~^^^^^^^^^^^^^^^^^^^^^^
[09:25:23]   File "D:\Program Files\Python\Lib\subprocess.py", line 1038, in __init__
[09:25:23]     self._execute_child(args, executable, preexec_fn, close_fds,
[09:25:23]     ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[09:25:23]                         pass_fds, cwd, env,
[09:25:23]                         ^^^^^^^^^^^^^^^^^^^
[09:25:23]     ...<5 lines>...
[09:25:23]                         gid, gids, uid, umask,
[09:25:23]                         ^^^^^^^^^^^^^^^^^^^^^^
[09:25:23]                         start_new_session, process_group)
[09:25:23]                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[09:25:23]   File "D:\Program Files\Python\Lib\subprocess.py", line 1550, in _execute_child
[09:25:23]     hp, ht, pid, tid = _winapi.CreateProcess(executable, args,
[09:25:23]                        ~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
[09:25:23]                              # no special security
[09:25:23]                              ^^^^^^^^^^^^^^^^^^^^^
[09:25:23]     ...<4 lines>...
[09:25:23]                              cwd,
[09:25:23]                              ^^^^
[09:25:23]                              startupinfo)
[09:25:23]                              ^^^^^^^^^^^^
[09:25:23] FileNotFoundError: [WinError 2] 系统找不到指定的文件

打包日志报错,这是咋回事
k08667891 发表于 2026-8-16 20:36
win10环境下测试了一下,自己编译了一次源码,运行起来和下载的没啥区别,但是有个小问题有点烦人,就是编译一次会在程序所在的目录生成一个build,导致调整完代码再次编译的时候,好像还是会直接读原来的build生成dist,必须得自己手动删除一次原build文件夹,也有可能我是个例
xixiaixuexi 发表于 2026-8-2 20:46
这个可以把windows上的py程序打包到ubuntu上用吗?
bingzhenxigua 发表于 2026-8-2 20:52
好好好 用心了
青春作伴 发表于 2026-8-2 21:03
可以可以,打包方便些
oscf 发表于 2026-8-2 21:20
感谢,以前直接命令行打包的,试试这个
wt580 发表于 2026-8-2 21:21
谢谢分享!
qybin963 发表于 2026-8-2 21:27
厉害,感谢分享!下载试用

免费评分

参与人数 1吾爱币 -15 违规 +1 收起 理由
L_Monkey -15 + 1 本次共删除【19】条灌水内容,结合之前的违规记录合并处理违规+1和CB-15

查看全部评分

daixiangjiang 发表于 2026-8-2 21:49
牛牛牛~~~~~
Shenxia 发表于 2026-8-2 22:23
感谢分享
hanck521 发表于 2026-8-2 22:38
厉害,感谢分享!
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-9-18 22:49

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表