技术方案-Python版_20251109171907.md 51 KB

技术方案 - Python版本

核心语言改用Python的影响分析

一、功能影响评估

功能模块 C++方案 Python方案 影响程度
桌面窗口管理 ✅ QMainWindow ✅ PyQt6/PySide6 ✅ 无影响
登录界面 ✅ Qt Widgets/Web ✅ PyQt6 Widgets/WebEngine ✅ 无影响
主界面布局 ✅ Qt Layout ✅ PyQt6 Layout ✅ 无影响
Chrome浏览器自动化 ✅ QWebEngineView ✅ Selenium/Playwright ✅ 无影响(甚至更简单)
Chrome插件支持 ✅ QWebEngineProfile ⚠️ 需要额外处理 ⚠️ 轻微影响
Steam集成 ✅ 进程调用/API ✅ subprocess/requests ✅ 无影响
自动更新 ✅ QNetworkAccessManager ✅ requests + 自定义 ✅ 无影响
窗口大小控制 ✅ QMainWindow API ✅ PyQt6 API ✅ 无影响
多Tab页系统 ✅ QTabWidget ✅ QTabWidget ✅ 无影响

结论:功能上基本无影响,Python完全可以实现所有需求!


二、技术栈对比

Python方案技术栈

技术组件 选型 说明
桌面框架 PyQt6 / PySide6 Qt的Python绑定,功能与C++版完全一致
编程语言 Python 3.10+ 开发效率高,代码简洁
浏览器自动化 Selenium / Playwright 比QWebEngine更强大的自动化能力
Web界面 QWebEngineView 支持HTML/React界面
加密库 cryptography / pycryptodome Python加密库,功能完整
打包工具 PyInstaller / cx_Freeze 打包为exe
代码保护 PyArmor / Nuitka 代码混淆和编译保护

三、详细实现方案

3.1 项目结构

buff-python-app/
├── requirements.txt
├── setup.py
├── main.py                    # 应用入口
├── src/
│   ├── core/                  # 核心逻辑(Python)
│   │   ├── auth/              # 认证模块
│   │   │   ├── license.py     # 许可证验证
│   │   │   ├── crypto.py      # 加密算法
│   │   │   └── hwid.py        # 硬件ID
│   │   ├── steam/             # Steam集成
│   │   │   ├── authenticator.py  # Steam验证器
│   │   │   └── trade.py       # 交易确认
│   │   ├── browser/           # 浏览器控制
│   │   │   ├── automation.py  # 自动化引擎(Selenium/Playwright)
│   │   │   └── extension.py  # 插件管理
│   │   └── updater/           # 更新模块
│   │       └── updater.py
│   ├── ui/                    # 界面层
│   │   ├── main_window.py     # 主窗口
│   │   ├── login_window.py    # 登录窗口
│   │   ├── components/        # UI组件
│   │   │   ├── header.py      # 顶部栏
│   │   │   ├── sidebar.py     # 左侧菜单
│   │   │   └── tab_view.py    # Tab页系统
│   │   └── webview.py         # Web视图容器
│   └── utils/                 # 工具函数
│       ├── config.py
│       └── logger.py
├── resources/
│   ├── chromium/              # Chrome浏览器
│   ├── extensions/            # Chrome插件
│   └── sda/                   # Steam工具
└── dist/                      # 构建输出

3.2 核心代码实现

主窗口管理

# main_window.py
from PyQt6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QHBoxLayout
from PyQt6.QtCore import Qt
from ui.components.header import Header
from ui.components.sidebar import Sidebar
from ui.components.tab_view import TabView

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Buff App")
        self.setMinimumSize(1000, 600)
        self.resize(1200, 800)
        
        # 创建中央部件
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        
        # 主布局
        main_layout = QVBoxLayout(central_widget)
        main_layout.setContentsMargins(0, 0, 0, 0)
        main_layout.setSpacing(0)
        
        # 顶部栏
        self.header = Header()
        main_layout.addWidget(self.header)
        
        # 内容区域(左侧菜单 + 右侧Tab)
        content_layout = QHBoxLayout()
        content_layout.setContentsMargins(0, 0, 0, 0)
        content_layout.setSpacing(0)
        
        # 左侧菜单
        self.sidebar = Sidebar()
        content_layout.addWidget(self.sidebar, 0)  # 固定宽度
        
        # 右侧Tab页
        self.tab_view = TabView()
        content_layout.addWidget(self.tab_view, 1)  # 可伸缩
        
        main_layout.addLayout(content_layout)
        
        # 连接信号
        self.header.exit_clicked.connect(self.close)
        self.sidebar.menu_clicked.connect(self.tab_view.add_tab)
        
    def resize_window(self, width: int, height: int):
        """通过按钮控制窗口大小"""
        self.resize(width, height)
    
    def maximize_window(self):
        """最大化/还原窗口"""
        if self.isMaximized():
            self.showNormal()
        else:
            self.showMaximized()

Chrome浏览器自动化(使用Playwright)

# browser/automation.py
from playwright.sync_api import sync_playwright, Browser, Page, BrowserContext
from pathlib import Path
import os
import sys
from typing import Optional, List, Dict
from src.utils.logger import get_logger

logger = get_logger(__name__)

class BrowserAutomation:
    def __init__(self):
        self.browser: Optional[Browser] = None
        self.context: Optional[BrowserContext] = None
        self.page: Optional[Page] = None
        self.playwright = None
        
    def launch_browser(self, headless: bool = False, extensions: List[str] = None):
        """启动浏览器"""
        try:
        self.playwright = sync_playwright().start()
        
        # Chrome路径(打包后的资源路径)
        chrome_path = self._get_chrome_path()
        
            if not os.path.exists(chrome_path):
                raise FileNotFoundError(f"Chrome浏览器未找到: {chrome_path}")
            
            # 准备启动参数
            launch_args = [
                '--disable-web-security',
                '--disable-features=IsolateOrigins,site-per-process',
                '--disable-blink-features=AutomationControlled',
                '--no-first-run',
                '--no-default-browser-check'
            ]
            
            # 加载插件
            if extensions:
                for ext_path in extensions:
                    if os.path.exists(ext_path):
                        launch_args.append(f'--load-extension={ext_path}')
        
        self.browser = self.playwright.chromium.launch(
            executable_path=chrome_path,
            headless=headless,
                args=launch_args
            )
            
            # 创建上下文
            context_options = {
                'viewport': {'width': 1920, 'height': 1080},
                'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
            }
            
            self.context = self.browser.new_context(**context_options)
            self.page = self.context.new_page()
            
            logger.info("浏览器启动成功")
        return self.page
            
        except Exception as e:
            logger.error(f"浏览器启动失败: {e}")
            raise
    
    def _get_chrome_path(self) -> str:
        """获取Chrome浏览器路径"""
        if getattr(sys, 'frozen', False):
            # 打包后的路径
            base_path = sys._MEIPASS
        else:
            # 开发环境
            base_path = Path(__file__).parent.parent.parent
        
        # 尝试多个可能的路径
        possible_paths = [
            Path(base_path) / "resources" / "chromium" / "chrome.exe",
            Path(base_path) / "resources" / "chromium" / "chrome-win" / "chrome.exe",
            Path(os.path.dirname(sys.executable)) / "resources" / "chromium" / "chrome.exe"
        ]
        
        for path in possible_paths:
            if path.exists():
                return str(path)
        
        # 如果都找不到,返回默认路径
        return str(possible_paths[0])
    
    def load_extensions(self, extension_paths: List[str]):
        """加载Chrome插件"""
        if not self.browser:
            raise RuntimeError("浏览器未启动")
        
        for ext_path in extension_paths:
            if not os.path.exists(ext_path):
                logger.warning(f"插件路径不存在: {ext_path}")
                continue
            
            # 重新创建上下文以加载插件
            context_options = {
                'viewport': {'width': 1920, 'height': 1080}
            }
            
            # Playwright通过启动参数加载插件,需要重启浏览器
            logger.info(f"加载插件: {ext_path}")
    
    def navigate(self, url: str, wait_until: str = "load"):
        """导航到URL"""
        if not self.page:
            raise RuntimeError("页面未创建")
        try:
            self.page.goto(url, wait_until=wait_until)
            logger.info(f"导航到: {url}")
        except Exception as e:
            logger.error(f"导航失败: {e}")
            raise
    
    def fill_form(self, form_data: Dict[str, str]):
        """填充表单"""
        if not self.page:
            raise RuntimeError("页面未创建")
        
        for selector, value in form_data.items():
            try:
            self.page.fill(selector, value)
                logger.debug(f"填充表单字段: {selector}")
            except Exception as e:
                logger.error(f"填充表单失败 {selector}: {e}")
                raise
    
    def click_element(self, selector: str, timeout: int = 30000):
        """点击元素"""
        if not self.page:
            raise RuntimeError("页面未创建")
        try:
            self.page.click(selector, timeout=timeout)
            logger.debug(f"点击元素: {selector}")
        except Exception as e:
            logger.error(f"点击元素失败 {selector}: {e}")
            raise
    
    def wait_for_element(self, selector: str, timeout: int = 30000):
        """等待元素出现"""
        if not self.page:
            raise RuntimeError("页面未创建")
        try:
            self.page.wait_for_selector(selector, timeout=timeout)
            logger.debug(f"元素已出现: {selector}")
        except Exception as e:
            logger.error(f"等待元素超时 {selector}: {e}")
            raise
    
    def execute_script(self, script: str):
        """执行JavaScript"""
        if not self.page:
            raise RuntimeError("页面未创建")
        try:
            result = self.page.evaluate(script)
            logger.debug("执行JavaScript脚本")
            return result
        except Exception as e:
            logger.error(f"执行脚本失败: {e}")
            raise
    
    def screenshot(self, path: str, full_page: bool = False):
        """截图"""
        if not self.page:
            raise RuntimeError("页面未创建")
        try:
            self.page.screenshot(path=path, full_page=full_page)
            logger.info(f"截图保存: {path}")
        except Exception as e:
            logger.error(f"截图失败: {e}")
            raise
    
    def get_page_content(self) -> str:
        """获取页面内容"""
        if not self.page:
            raise RuntimeError("页面未创建")
        return self.page.content()
    
    def close(self):
        """关闭浏览器"""
        try:
            if self.page:
                self.page.close()
            if self.context:
                self.context.close()
        if self.browser:
            self.browser.close()
        if self.playwright:
            self.playwright.stop()
            logger.info("浏览器已关闭")
        except Exception as e:
            logger.error(f"关闭浏览器失败: {e}")

Steam集成

# steam/authenticator.py
import subprocess
import json
import hmac
import hashlib
import time
import base64
import os
import sys
from pathlib import Path
from typing import Optional
from src.utils.logger import get_logger

logger = get_logger(__name__)

class SteamAuthenticator:
    def __init__(self):
        self.sda_path = self._get_sda_path()
        self.accounts = {}
        self.mafiles_path = self._get_mafiles_path()
    
    def _get_sda_path(self) -> str:
        """获取SDA路径"""
        if getattr(sys, 'frozen', False):
            base_path = sys._MEIPASS
        else:
            base_path = Path(__file__).parent.parent.parent
        
        possible_paths = [
            Path(base_path) / "resources" / "sda" / "SteamDesktopAuthenticator.exe",
            Path(os.path.dirname(sys.executable)) / "resources" / "sda" / "SteamDesktopAuthenticator.exe"
        ]
        
        for path in possible_paths:
            if path.exists():
                return str(path)
        
        return str(possible_paths[0])
    
    def _get_mafiles_path(self) -> Path:
        """获取maFiles配置目录"""
        if sys.platform == 'win32':
            appdata = os.getenv('APPDATA', '')
            return Path(appdata) / "SteamDesktopAuthenticator" / "maFiles"
        else:
            return Path.home() / ".steam" / "maFiles"
    
    def get_guard_code(self, username: str) -> Optional[str]:
        """获取Steam Guard验证码"""
        # 方法1:调用SDA命令行
        if os.path.exists(self.sda_path):
        try:
            result = subprocess.run(
                [self.sda_path, '--user', username, '--get-code'],
                capture_output=True,
                text=True,
                timeout=10
            )
            if result.returncode == 0:
                    code = result.stdout.strip()
                    logger.info(f"从SDA获取验证码成功: {username}")
                    return code
            except subprocess.TimeoutExpired:
                logger.error(f"SDA调用超时: {username}")
        except Exception as e:
                logger.error(f"SDA调用失败: {e}")
        
        # 方法2:读取配置文件自行生成
        code = self._generate_code_from_config(username)
        if code:
            logger.info(f"从配置文件生成验证码成功: {username}")
        else:
            logger.error(f"无法获取验证码: {username}")
        
        return code
    
    def _generate_code_from_config(self, username: str) -> Optional[str]:
        """从SDA配置文件生成验证码"""
        config_path = self.mafiles_path / f"{username}.maFile"
        
        if not config_path.exists():
            logger.warning(f"配置文件不存在: {config_path}")
            return None
        
        try:
        with open(config_path, 'r', encoding='utf-8') as f:
            config = json.load(f)
        
        shared_secret = config.get('shared_secret', '')
        if not shared_secret:
                logger.error(f"配置文件中缺少shared_secret: {username}")
                return None
        
        # Steam TOTP算法
        return self._generate_steam_totp(shared_secret)
        except json.JSONDecodeError as e:
            logger.error(f"配置文件解析失败: {e}")
            return None
        except Exception as e:
            logger.error(f"读取配置文件失败: {e}")
            return None
    
    def _generate_steam_totp(self, shared_secret: str) -> str:
        """生成Steam TOTP验证码"""
        steam_chars = "23456789BCDFGHJKMNPQRTVWXY"
        
        current_time = int(time.time())
        time_code = current_time // 30
        
        # 转换为大端字节序
        time_buffer = time_code.to_bytes(8, byteorder='big')
        
        # HMAC-SHA1
        try:
        secret_bytes = base64.b64decode(shared_secret)
        except Exception as e:
            logger.error(f"Base64解码失败: {e}")
            return ""
        
        hmac_digest = hmac.new(secret_bytes, time_buffer, hashlib.sha1).digest()
        
        # 生成5位代码
        start = hmac_digest[19] & 0x0F
        full_code = int.from_bytes(hmac_digest[start:start+4], byteorder='big') & 0x7FFFFFFF
        
        code = ""
        for _ in range(5):
            code += steam_chars[full_code % len(steam_chars)]
            full_code //= len(steam_chars)
        
        return code
    
    def confirm_trade(self, username: str, trade_id: str) -> bool:
        """确认Steam交易"""
        if not os.path.exists(self.sda_path):
            logger.error("SDA路径不存在")
            return False
        
        try:
            result = subprocess.run(
                [self.sda_path, '--confirm-trade', trade_id, '--user', username],
                capture_output=True,
                text=True,
                timeout=10
            )
            
            if result.returncode == 0:
                logger.info(f"交易确认成功: {trade_id}")
                return True
            else:
                logger.error(f"交易确认失败: {result.stderr}")
                return False
        except subprocess.TimeoutExpired:
            logger.error("交易确认超时")
            return False
        except Exception as e:
            logger.error(f"交易确认异常: {e}")
            return False
    
    def list_accounts(self) -> list:
        """列出所有已配置的Steam账号"""
        accounts = []
        if not self.mafiles_path.exists():
            return accounts
        
        for file in self.mafiles_path.glob("*.maFile"):
            try:
                with open(file, 'r', encoding='utf-8') as f:
                    config = json.load(f)
                    account_name = config.get('account_name', file.stem)
                    accounts.append(account_name)
            except Exception as e:
                logger.warning(f"读取账号配置失败 {file}: {e}")
        
        return accounts

许可证验证

# core/auth/license.py
import hashlib
import hmac
import requests
import platform
import subprocess
import base64
import uuid
import json
import time
from pathlib import Path
from typing import Optional
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.backends import default_backend
from src.utils.logger import get_logger
from src.utils.config import get_config

logger = get_logger(__name__)

class LicenseValidator:
    def __init__(self):
        config = get_config()
        self.api_url = config.get('license', {}).get('api_url', 'https://your-api.com/validate')
        self.public_key = self._load_public_key()
        self.cache_file = Path.home() / ".buff_app" / "license_cache.json"
        self.cache_file.parent.mkdir(parents=True, exist_ok=True)
    
    def get_hardware_id(self) -> str:
        """获取硬件ID"""
        components = []
        
        # CPU ID
        try:
            result = subprocess.run(
                ['wmic', 'cpu', 'get', 'ProcessorId'],
                capture_output=True,
                text=True,
                timeout=5
            )
            if result.returncode == 0:
                lines = result.stdout.strip().split('\n')
                if len(lines) > 1:
                    cpu_id = lines[1].strip()
                    if cpu_id:
            components.append(cpu_id)
        except Exception as e:
            logger.warning(f"获取CPU ID失败: {e}")
            components.append(platform.processor() or "unknown")
        
        # 主板序列号
        try:
            result = subprocess.run(
                ['wmic', 'baseboard', 'get', 'SerialNumber'],
                capture_output=True,
                text=True,
                timeout=5
            )
            if result.returncode == 0:
                lines = result.stdout.strip().split('\n')
                if len(lines) > 1:
                    board_id = lines[1].strip()
                    if board_id:
            components.append(board_id)
        except Exception as e:
            logger.warning(f"获取主板序列号失败: {e}")
            components.append(platform.machine() or "unknown")
        
        # MAC地址
        try:
            mac = uuid.getnode()
            mac_str = ':'.join(['{:02x}'.format((mac >> elements) & 0xff) 
                              for elements in range(0, 2*6, 2)][::-1])
            components.append(mac_str)
        except Exception as e:
            logger.warning(f"获取MAC地址失败: {e}")
            components.append("unknown")
        
        # 生成硬件ID
        combined = '|'.join(components)
        hwid = hashlib.sha256(combined.encode()).hexdigest()
        logger.debug(f"硬件ID生成成功: {hwid[:16]}...")
        return hwid
    
    def validate_online(self, license_key: str) -> bool:
        """在线验证许可证"""
        hwid = self.get_hardware_id()
        
        try:
            response = requests.post(
                self.api_url,
                json={
                    'license': license_key,
                    'hwid': hwid,
                    'version': '1.0.0'
                },
                timeout=10
            )
            
            if response.status_code == 200:
                data = response.json()
                # 验证服务器签名
                if data.get('valid') and self._verify_signature(data):
                    # 缓存验证结果
                    self._cache_validation(license_key, True)
                    logger.info("许可证验证成功")
                    return True
                else:
                    logger.warning("许可证验证失败:签名无效")
            else:
                logger.error(f"许可证验证失败:HTTP {response.status_code}")
        except requests.exceptions.Timeout:
            logger.error("许可证验证超时")
        except requests.exceptions.RequestException as e:
            logger.error(f"许可证验证请求失败: {e}")
            # 网络失败时使用缓存
            return self._check_cache(license_key)
        except Exception as e:
            logger.error(f"许可证验证异常: {e}")
        
        return False
    
    def _verify_signature(self, data: dict) -> bool:
        """验证RSA签名"""
        if not self.public_key:
            logger.warning("公钥未加载,跳过签名验证")
            return True  # 如果公钥未配置,允许通过(开发环境)
        
        try:
            message = json.dumps(data.get('data', {}), sort_keys=True).encode()
            signature_str = data.get('signature', '')
            
            if not signature_str:
                return False
            
            signature = base64.b64decode(signature_str)
            
            self.public_key.verify(
                signature,
                message,
                padding.PSS(
                    mgf=padding.MGF1(hashes.SHA256()),
                    salt_length=padding.PSS.MAX_LENGTH
                ),
                hashes.SHA256()
            )
            return True
        except Exception as e:
            logger.error(f"签名验证失败: {e}")
            return False
    
    def _load_public_key(self):
        """加载RSA公钥"""
        try:
            # 从配置文件或资源文件加载
        public_key_pem = """
        -----BEGIN PUBLIC KEY-----
        MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
        -----END PUBLIC KEY-----
        """
            # 实际应从配置文件或资源文件读取
            public_key = serialization.load_pem_public_key(
                public_key_pem.encode(),
                backend=default_backend()
            )
            logger.info("RSA公钥加载成功")
            return public_key
        except Exception as e:
            logger.warning(f"公钥加载失败: {e}")
            return None
    
    def _cache_validation(self, license_key: str, valid: bool):
        """缓存验证结果"""
        try:
            cache_data = {
                'license': license_key,
                'valid': valid,
                'timestamp': int(time.time())
            }
            with open(self.cache_file, 'w') as f:
                json.dump(cache_data, f)
        except Exception as e:
            logger.warning(f"缓存验证结果失败: {e}")
    
    def _check_cache(self, license_key: str) -> bool:
        """检查缓存的验证结果"""
        try:
            if not self.cache_file.exists():
                return False
            
            with open(self.cache_file, 'r') as f:
                cache_data = json.load(f)
            
            # 检查缓存是否过期(24小时)
            if int(time.time()) - cache_data.get('timestamp', 0) > 86400:
                return False
            
            # 检查许可证是否匹配
            if cache_data.get('license') != license_key:
                return False
            
            return cache_data.get('valid', False)
        except Exception as e:
            logger.warning(f"读取缓存失败: {e}")
            return False

工具模块

# utils/logger.py
import logging
import sys
from pathlib import Path
from logging.handlers import RotatingFileHandler

_loggers = {}

def get_logger(name: str = __name__) -> logging.Logger:
    """获取日志记录器"""
    if name in _loggers:
        return _loggers[name]
    
    logger = logging.getLogger(name)
    logger.setLevel(logging.DEBUG)
    
    # 避免重复添加handler
    if logger.handlers:
        _loggers[name] = logger
        return logger
    
    # 控制台输出
    console_handler = logging.StreamHandler(sys.stdout)
    console_handler.setLevel(logging.INFO)
    console_format = logging.Formatter(
        '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S'
    )
    console_handler.setFormatter(console_format)
    logger.addHandler(console_handler)
    
    # 文件输出
    log_dir = Path.home() / ".buff_app" / "logs"
    log_dir.mkdir(parents=True, exist_ok=True)
    log_file = log_dir / "app.log"
    
    file_handler = RotatingFileHandler(
        log_file,
        maxBytes=10*1024*1024,  # 10MB
        backupCount=5
    )
    file_handler.setLevel(logging.DEBUG)
    file_format = logging.Formatter(
        '%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S'
    )
    file_handler.setFormatter(file_format)
    logger.addHandler(file_handler)
    
    _loggers[name] = logger
    return logger
# utils/config.py
import json
import os
import sys
from pathlib import Path
from typing import Dict, Any

_config = None

def get_config() -> Dict[str, Any]:
    """获取配置"""
    global _config
    if _config is not None:
        return _config
    
    # 默认配置
    default_config = {
        'app': {
            'name': 'Buff App',
            'version': '1.0.0'
        },
        'license': {
            'api_url': 'https://your-api.com/validate',
            'cache_duration': 86400  # 24小时
        },
        'browser': {
            'headless': False,
            'timeout': 30000
        },
        'steam': {
            'sda_path': 'resources/sda/SteamDesktopAuthenticator.exe'
        },
        'updater': {
            'check_interval': 3600,  # 1小时
            'update_url': 'https://your-server.com/updates'
        }
    }
    
    # 从配置文件加载
    config_file = Path.home() / ".buff_app" / "config.json"
    if config_file.exists():
        try:
            with open(config_file, 'r', encoding='utf-8') as f:
                user_config = json.load(f)
                # 合并配置
                _config = _merge_config(default_config, user_config)
        except Exception as e:
            print(f"加载配置文件失败: {e}")
            _config = default_config
    else:
        _config = default_config
        # 保存默认配置
        config_file.parent.mkdir(parents=True, exist_ok=True)
        try:
            with open(config_file, 'w', encoding='utf-8') as f:
                json.dump(_config, f, indent=2, ensure_ascii=False)
        except Exception:
            pass
    
    return _config

def _merge_config(default: Dict, user: Dict) -> Dict:
    """合并配置"""
    result = default.copy()
    for key, value in user.items():
        if key in result and isinstance(result[key], dict) and isinstance(value, dict):
            result[key] = _merge_config(result[key], value)
        else:
            result[key] = value
    return result

def save_config(config: Dict[str, Any]):
    """保存配置"""
    global _config
    _config = config
    config_file = Path.home() / ".buff_app" / "config.json"
    config_file.parent.mkdir(parents=True, exist_ok=True)
    with open(config_file, 'w', encoding='utf-8') as f:
        json.dump(config, f, indent=2, ensure_ascii=False)

自动更新模块

# core/updater/updater.py
import requests
import json
import sys
import os
import subprocess
from pathlib import Path
from typing import Optional, Dict
from src.utils.logger import get_logger
from src.utils.config import get_config

logger = get_logger(__name__)

class AppUpdater:
    def __init__(self):
        config = get_config()
        self.update_url = config.get('updater', {}).get('update_url', '')
        self.current_version = config.get('app', {}).get('version', '1.0.0')
        self.check_interval = config.get('updater', {}).get('check_interval', 3600)
    
    def check_update(self) -> Optional[Dict]:
        """检查更新"""
        if not self.update_url:
            logger.warning("更新URL未配置")
            return None
        
        try:
            response = requests.get(
                f"{self.update_url}/version.json",
                timeout=10
            )
            
            if response.status_code == 200:
                update_info = response.json()
                latest_version = update_info.get('version', '')
                
                if self._compare_version(latest_version, self.current_version) > 0:
                    logger.info(f"发现新版本: {latest_version}")
                    return update_info
                else:
                    logger.debug("当前已是最新版本")
                    return None
            else:
                logger.error(f"检查更新失败: HTTP {response.status_code}")
        except requests.exceptions.RequestException as e:
            logger.error(f"检查更新请求失败: {e}")
        except Exception as e:
            logger.error(f"检查更新异常: {e}")
        
        return None
    
    def download_update(self, update_info: Dict) -> bool:
        """下载更新"""
        download_url = update_info.get('download_url', '')
        if not download_url:
            logger.error("下载URL不存在")
            return False
        
        try:
            # 创建临时目录
            temp_dir = Path.home() / ".buff_app" / "temp"
            temp_dir.mkdir(parents=True, exist_ok=True)
            
            # 下载文件
            logger.info("开始下载更新...")
            response = requests.get(download_url, stream=True, timeout=60)
            response.raise_for_status()
            
            update_file = temp_dir / "update.exe"
            total_size = int(response.headers.get('content-length', 0))
            downloaded = 0
            
            with open(update_file, 'wb') as f:
                for chunk in response.iter_content(chunk_size=8192):
                    if chunk:
                        f.write(chunk)
                        downloaded += len(chunk)
                        # 可以发送进度信号
                        if total_size > 0:
                            progress = (downloaded / total_size) * 100
                            logger.debug(f"下载进度: {progress:.1f}%")
            
            logger.info("更新文件下载完成")
            return True
            
        except Exception as e:
            logger.error(f"下载更新失败: {e}")
            return False
    
    def install_update(self, update_file: Path) -> bool:
        """安装更新"""
        try:
            # 执行更新安装程序
            subprocess.Popen([str(update_file)], shell=True)
            logger.info("更新安装程序已启动")
            return True
        except Exception as e:
            logger.error(f"安装更新失败: {e}")
            return False
    
    def _compare_version(self, v1: str, v2: str) -> int:
        """比较版本号,返回: 1 if v1 > v2, -1 if v1 < v2, 0 if v1 == v2"""
        def version_tuple(v):
            return tuple(map(int, v.split('.')))
        
        try:
            t1 = version_tuple(v1)
            t2 = version_tuple(v2)
            if t1 > t2:
                return 1
            elif t1 < t2:
                return -1
            else:
                return 0
        except Exception:
            return 0

主程序入口

# main.py
import sys
import os
from pathlib import Path
from PyQt6.QtWidgets import QApplication
from PyQt6.QtCore import Qt
from src.ui.login_window import LoginWindow
from src.ui.main_window import MainWindow
from src.core.auth.license import LicenseValidator
from src.core.updater.updater import AppUpdater
from src.utils.logger import get_logger
from src.utils.config import get_config

logger = get_logger(__name__)

def main():
    """主程序入口"""
    # 设置高DPI支持
    QApplication.setHighDpiScaleFactorRoundingPolicy(
        Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
    )
    
    app = QApplication(sys.argv)
    app.setApplicationName("Buff App")
    app.setOrganizationName("Your Company")
    
    # 检查许可证
    license_validator = LicenseValidator()
    license_key = os.getenv('BUFF_LICENSE_KEY', '')
    
    if not license_key:
        # 从配置文件读取
        config = get_config()
        license_key = config.get('license', {}).get('key', '')
    
    if license_key:
        if not license_validator.validate_online(license_key):
            logger.error("许可证验证失败")
            # 显示错误对话框
            from PyQt6.QtWidgets import QMessageBox
            msg = QMessageBox()
            msg.setIcon(QMessageBox.Icon.Critical)
            msg.setWindowTitle("许可证错误")
            msg.setText("许可证验证失败,程序无法启动。")
            msg.exec()
            sys.exit(1)
    else:
        logger.warning("未找到许可证密钥,跳过验证(开发模式)")
    
    # 检查更新(后台进行)
    updater = AppUpdater()
    update_info = updater.check_update()
    if update_info:
        logger.info(f"发现新版本: {update_info.get('version')}")
        # 可以显示更新提示对话框
    
    # 显示登录窗口
    login_window = LoginWindow()
    
    if login_window.exec() == LoginWindow.DialogCode.Accepted:
        # 登录成功,显示主窗口
        main_window = MainWindow()
        main_window.show()
        sys.exit(app.exec())
    else:
        # 登录取消或失败
        sys.exit(0)

if __name__ == '__main__':
    main()

登录窗口

# ui/login_window.py
from PyQt6.QtWidgets import (
    QDialog, QVBoxLayout, QHBoxLayout, QLabel, 
    QLineEdit, QPushButton, QMessageBox
)
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QFont
from src.utils.logger import get_logger

logger = get_logger(__name__)

class LoginWindow(QDialog):
    """登录窗口"""
    login_success = pyqtSignal(str, str)  # username, password
    
    def __init__(self):
        super().__init__()
        self.setWindowTitle("登录 - Buff App")
        self.setFixedSize(400, 300)
        self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
        
        self._init_ui()
    
    def _init_ui(self):
        layout = QVBoxLayout()
        layout.setSpacing(15)
        layout.setContentsMargins(30, 30, 30, 30)
        
        # 标题
        title = QLabel("Buff App")
        title.setAlignment(Qt.AlignmentFlag.AlignCenter)
        title_font = QFont()
        title_font.setPointSize(20)
        title_font.setBold(True)
        title.setFont(title_font)
        layout.addWidget(title)
        
        # 用户名
        username_label = QLabel("用户名:")
        layout.addWidget(username_label)
        self.username_input = QLineEdit()
        self.username_input.setPlaceholderText("请输入用户名")
        layout.addWidget(self.username_input)
        
        # 密码
        password_label = QLabel("密码:")
        layout.addWidget(password_label)
        self.password_input = QLineEdit()
        self.password_input.setPlaceholderText("请输入密码")
        self.password_input.setEchoMode(QLineEdit.EchoMode.Password)
        layout.addWidget(self.password_input)
        
        # 按钮
        button_layout = QHBoxLayout()
        login_btn = QPushButton("登录")
        login_btn.clicked.connect(self._handle_login)
        cancel_btn = QPushButton("取消")
        cancel_btn.clicked.connect(self.reject)
        button_layout.addWidget(login_btn)
        button_layout.addWidget(cancel_btn)
        layout.addLayout(button_layout)
        
        self.setLayout(layout)
    
    def _handle_login(self):
        """处理登录"""
        username = self.username_input.text().strip()
        password = self.password_input.text().strip()
        
        if not username or not password:
            QMessageBox.warning(self, "提示", "请输入用户名和密码")
            return
        
        # 这里应该调用实际的登录验证逻辑
        # 示例:简单的验证
        if self._validate_login(username, password):
            logger.info(f"用户登录成功: {username}")
            self.login_success.emit(username, password)
            self.accept()
        else:
            QMessageBox.warning(self, "登录失败", "用户名或密码错误")
    
    def _validate_login(self, username: str, password: str) -> bool:
        """验证登录(示例实现)"""
        # 实际应该调用API验证
        # 这里只是示例
        return len(username) > 0 and len(password) > 0

项目配置文件

# requirements.txt
PyQt6==6.6.0
PyQt6-Qt6==6.6.0
PyQt6-sip==13.6.0
playwright==1.40.0
cryptography==41.0.7
requests==2.31.0
pyinstaller==6.3.0
# setup.py
from setuptools import setup, find_packages

setup(
    name="buff-app",
    version="1.0.0",
    description="Buff App - Python Desktop Application",
    author="Your Name",
    packages=find_packages(),
    install_requires=[
        "PyQt6>=6.6.0",
        "playwright>=1.40.0",
        "cryptography>=41.0.0",
        "requests>=2.31.0",
    ],
    python_requires=">=3.10",
    entry_points={
        "console_scripts": [
            "buff-app=main:main",
        ],
    },
)

四、关键差异和影响

4.1 优势 ✅

方面 Python优势
开发效率 ⭐⭐⭐⭐⭐ 代码简洁,开发速度快
浏览器自动化 ⭐⭐⭐⭐⭐ Selenium/Playwright生态成熟,功能强大
第三方库 ⭐⭐⭐⭐⭐ PyPI库丰富,Steam API、加密等都有现成库
调试便利 ⭐⭐⭐⭐⭐ Python调试工具完善,开发体验好
学习曲线 ⭐⭐⭐⭐⭐ Python语法简单,团队上手快

4.2 劣势 ⚠️

方面 Python劣势 影响程度
安全性 ⭐⭐ Python容易被反编译 ⚠️ 中等影响(需用PyArmor/Nuitka保护)
性能 ⭐⭐⭐ 比C++慢,但桌面应用足够 ✅ 影响较小
打包体积 ⭐⭐⭐ 包含Python运行时,体积较大 ⚠️ 轻微影响(约50-100MB)
启动速度 ⭐⭐⭐ 比C++慢,但可接受 ✅ 影响较小
Chrome插件加载 ⭐⭐⭐ 需要额外处理 ⚠️ 轻微影响(Playwright支持)

五、安全性解决方案

5.1 代码保护方案

方案A:使用PyArmor(推荐)

# 安装
pip install pyarmor

# 混淆保护
pyarmor gen --enable-rft main.py

# 打包
pyinstaller --onefile --key=your-key main.py

优点:

  • 代码混淆,难以直接阅读
  • 支持加密字符串和函数
  • 使用简单

缺点:

  • 仍可被专业工具反编译(但难度增加)

方案B:使用Nuitka编译为C++(最安全)

# 安装
pip install nuitka

# 编译为exe(会先编译为C++再编译为二进制)
python -m nuitka --onefile --windows-icon=icon.ico main.py

优点:

  • 编译为真正的二进制,接近C++安全性
  • 性能提升
  • 难以反编译

缺点:

  • 编译时间长
  • 打包体积较大

方案C:混合方案(推荐)

# 1. 核心敏感代码用Nuitka编译为扩展模块
# core/auth/license_core.py -> license_core.pyd

# 2. 其他代码用PyArmor保护
pyarmor gen --enable-rft src/

# 3. 打包
pyinstaller --onefile main.py

六、UI组件实现

6.1 Tab页系统

# ui/components/tab_view.py
from PyQt6.QtWidgets import QWidget, QTabWidget, QVBoxLayout, QPushButton
from PyQt6.QtCore import pyqtSignal
from src.utils.logger import get_logger

logger = get_logger(__name__)

class TabView(QWidget):
    """Tab页视图组件"""
    tab_closed = pyqtSignal(int)
    
    def __init__(self):
        super().__init__()
        self.tabs = {}
        self._init_ui()
    
    def _init_ui(self):
        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        
        self.tab_widget = QTabWidget()
        self.tab_widget.setTabsClosable(True)
        self.tab_widget.tabCloseRequested.connect(self._close_tab)
        layout.addWidget(self.tab_widget)
        
        self.setLayout(layout)
    
    def add_tab(self, title: str, content: QWidget, tab_id: str = None):
        """添加Tab页"""
        if tab_id is None:
            tab_id = title
        
        # 如果Tab已存在,切换到该Tab
        if tab_id in self.tabs:
            index = self.tab_widget.indexOf(self.tabs[tab_id])
            self.tab_widget.setCurrentIndex(index)
            return
        
        # 创建新Tab
        index = self.tab_widget.addTab(content, title)
        self.tabs[tab_id] = content
        self.tab_widget.setCurrentIndex(index)
        logger.debug(f"添加Tab页: {title}")
    
    def _close_tab(self, index: int):
        """关闭Tab页"""
        widget = self.tab_widget.widget(index)
        if widget:
            tab_id = None
            for tid, w in self.tabs.items():
                if w == widget:
                    tab_id = tid
                    break
            
            self.tab_widget.removeTab(index)
            if tab_id:
                del self.tabs[tab_id]
                self.tab_closed.emit(index)
            logger.debug(f"关闭Tab页: index={index}")
    
    def get_current_tab(self) -> QWidget:
        """获取当前Tab页"""
        return self.tab_widget.currentWidget()

6.2 侧边栏组件

# ui/components/sidebar.py
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QPushButton, QListWidget
from PyQt6.QtCore import pyqtSignal, Qt
from PyQt6.QtGui import QIcon
from src.utils.logger import get_logger

logger = get_logger(__name__)

class Sidebar(QWidget):
    """侧边栏组件"""
    menu_clicked = pyqtSignal(str)  # 发送菜单项ID
    
    def __init__(self):
        super().__init__()
        self.setFixedWidth(200)
        self.menu_items = {}
        self._init_ui()
    
    def _init_ui(self):
        layout = QVBoxLayout()
        layout.setContentsMargins(5, 5, 5, 5)
        layout.setSpacing(5)
        
        self.menu_list = QListWidget()
        self.menu_list.itemClicked.connect(self._on_menu_clicked)
        layout.addWidget(self.menu_list)
        
        self.setLayout(layout)
    
    def add_menu_item(self, item_id: str, text: str, icon: QIcon = None):
        """添加菜单项"""
        from PyQt6.QtWidgets import QListWidgetItem
        
        item = QListWidgetItem(text)
        if icon:
            item.setIcon(icon)
        item.setData(Qt.ItemDataRole.UserRole, item_id)
        
        self.menu_list.addItem(item)
        self.menu_items[item_id] = item
        logger.debug(f"添加菜单项: {item_id} - {text}")
    
    def _on_menu_clicked(self, item: QListWidgetItem):
        """菜单项点击处理"""
        item_id = item.data(Qt.ItemDataRole.UserRole)
        if item_id:
            self.menu_clicked.emit(item_id)
            logger.debug(f"菜单项被点击: {item_id}")

6.3 顶部栏组件

# ui/components/header.py
from PyQt6.QtWidgets import QWidget, QHBoxLayout, QLabel, QPushButton
from PyQt6.QtCore import pyqtSignal, Qt
from PyQt6.QtGui import QFont
from src.utils.logger import get_logger

logger = get_logger(__name__)

class Header(QWidget):
    """顶部栏组件"""
    exit_clicked = pyqtSignal()
    minimize_clicked = pyqtSignal()
    maximize_clicked = pyqtSignal()
    
    def __init__(self):
        super().__init__()
        self.setFixedHeight(60)
        self._init_ui()
    
    def _init_ui(self):
        layout = QHBoxLayout()
        layout.setContentsMargins(10, 5, 10, 5)
        
        # Logo/标题
        self.logo = QLabel("Buff App")
        logo_font = QFont()
        logo_font.setPointSize(16)
        logo_font.setBold(True)
        self.logo.setFont(logo_font)
        layout.addWidget(self.logo)
        
        layout.addStretch()
        
        # 窗口控制按钮
        minimize_btn = QPushButton("—")
        minimize_btn.setFixedSize(30, 30)
        minimize_btn.clicked.connect(self.minimize_clicked.emit)
        layout.addWidget(minimize_btn)
        
        maximize_btn = QPushButton("□")
        maximize_btn.setFixedSize(30, 30)
        maximize_btn.clicked.connect(self.maximize_clicked.emit)
        layout.addWidget(maximize_btn)
        
        exit_btn = QPushButton("×")
        exit_btn.setFixedSize(30, 30)
        exit_btn.clicked.connect(self.exit_clicked.emit)
        layout.addWidget(exit_btn)
        
        self.setLayout(layout)

七、打包方案

7.1 使用PyInstaller

# BuffApp.spec
# -*- mode: python ; coding: utf-8 -*-

block_cipher = None

a = Analysis(
    ['main.py'],
    pathex=[],
    binaries=[],
    datas=[
        # PyQt6资源文件(自动收集)
        # ('resources', 'resources'),  # 如果需要包含资源文件
    ],
    hiddenimports=[
        'PyQt6',
        'PyQt6.QtCore',
        'PyQt6.QtWidgets',
        'PyQt6.QtGui',
        'playwright',
        'playwright.sync_api',
        'cryptography',
        'requests',
    ],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=[],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)

pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.zipfiles,
    a.datas,
    [],
    name='BuffApp',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    upx_exclude=[],
    runtime_tmpdir=None,
    console=False,  # 无控制台窗口
    icon='resources/icon.ico',
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
)

# 如果需要安装包,使用NSIS
# coll = COLLECT(...)

7.2 打包命令

# 1. 安装依赖
pip install -r requirements.txt
pip install pyinstaller

# 2. 安装Playwright浏览器
playwright install chromium

# 3. 复制Chrome到resources目录
# 从 %USERPROFILE%\AppData\Local\ms-playwright\ 复制到 resources/chromium/

# 4. 生成spec文件(如果还没有)
pyinstaller --name=BuffApp --windowed --icon=resources/icon.ico main.py

# 5. 编辑BuffApp.spec,添加资源文件配置

# 6. 打包
pyinstaller BuffApp.spec

# 7. 使用NSIS创建安装包(可选)
makensis installer.nsi

7.3 NSIS安装脚本

; installer.nsi
!include "MUI2.nsh"

Name "Buff App"
OutFile "BuffApp-Setup-1.0.0.exe"
InstallDir "$PROGRAMFILES\BuffApp"

; 设置压缩方式
SetCompressor /SOLID lzma

Section "MainSection" SEC01
    SetOutPath "$INSTDIR"
    
    ; 主程序
    File "dist\BuffApp.exe"
    
    ; Chrome浏览器
    SetOutPath "$INSTDIR\resources\chromium"
    File /r "resources\chromium\*"
    
    ; Chrome插件
    SetOutPath "$INSTDIR\resources\extensions"
    File /r "resources\extensions\*"
    
    ; Steam工具
    SetOutPath "$INSTDIR\resources\sda"
    File /r "resources\sda\*"
    
    ; 创建快捷方式
    CreateShortcut "$DESKTOP\Buff App.lnk" "$INSTDIR\BuffApp.exe"
    CreateShortcut "$SMPROGRAMS\Buff App.lnk" "$INSTDIR\BuffApp.exe"
    
    ; 写入注册表
    WriteRegStr HKLM "Software\BuffApp" "InstallDir" "$INSTDIR"
    WriteUninstaller "$INSTDIR\Uninstall.exe"
SectionEnd

八、性能对比

操作 C++方案 Python方案 差异
应用启动 ~0.5秒 ~1-2秒 慢1-1.5秒
窗口响应 即时 即时 无差异
浏览器启动 ~1秒 ~1-2秒 慢0.5-1秒
自动化操作 快速 快速 无差异
内存占用 ~100MB ~150-200MB 多50-100MB

结论:性能差异在桌面应用中可接受,用户体验无明显影响。


九、最终建议

✅ 推荐使用Python方案,如果:

  1. 开发效率优先 - 需要快速开发上线
  2. 团队熟悉Python - 降低学习成本
  3. 功能实现优先 - 所有功能都能实现
  4. 安全性要求中等 - 使用Nuitka+PyArmor可达到较好保护

⚠️ 不推荐Python方案,如果:

  1. 安全性要求极高 - 需要商业级防破解(如游戏外挂防护)
  2. 性能要求极高 - 需要毫秒级响应
  3. 打包体积敏感 - 需要小于50MB

十、Python方案总结

功能实现:✅ 完全无影响

  • 所有需求功能都能实现
  • 浏览器自动化甚至更简单(Playwright比QWebEngine更强大)
  • Steam集成、自动更新、窗口控制都能完美实现

安全性:⚠️ 需要额外保护

  • 使用Nuitka编译核心模块
  • 使用PyArmor混淆其他代码
  • 服务器端验证必不可少

性能:✅ 影响可接受

  • 启动慢1-2秒,但用户体验无影响
  • 运行时性能足够流畅

开发效率:✅ 显著提升

  • Python开发速度比C++快2-3倍
  • 代码量减少50%以上
  • 调试和维护更容易

十一、推荐技术栈(Python版)

PyQt6 / PySide6          # 桌面框架
Playwright               # 浏览器自动化(比Selenium更现代)
cryptography             # 加密库
requests                 # HTTP请求
PyInstaller              # 打包工具
Nuitka                   # 核心模块编译保护
PyArmor                  # 代码混淆

结论:Python完全可以替代C++,功能无影响,开发效率更高!