技术方案-Python版_20251109170150.md 24 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
from pathlib import Path
from cryptography.fernet import Fernet

class SteamAuthenticator:
    def __init__(self):
        self.sda_path = self._get_sda_path()
        self.accounts = {}
    
    def _get_sda_path(self) -> str:
        """获取SDA路径"""
        if getattr(sys, 'frozen', False):
            base_path = sys._MEIPASS
        else:
            base_path = os.path.dirname(__file__)
        
        sda_path = Path(base_path) / "resources" / "sda" / "SteamDesktopAuthenticator.exe"
        return str(sda_path)
    
    def get_guard_code(self, username: str) -> str:
        """获取Steam Guard验证码"""
        # 方法1:调用SDA命令行
        try:
            result = subprocess.run(
                [self.sda_path, '--user', username, '--get-code'],
                capture_output=True,
                text=True,
                timeout=10
            )
            if result.returncode == 0:
                return result.stdout.strip()
        except Exception as e:
            print(f"SDA调用失败: {e}")
        
        # 方法2:读取配置文件自行生成
        return self._generate_code_from_config(username)
    
    def _generate_code_from_config(self, username: str) -> str:
        """从SDA配置文件生成验证码"""
        config_path = Path.home() / "AppData" / "Roaming" / "SteamDesktopAuthenticator" / "maFiles" / f"{username}.maFile"
        
        if not config_path.exists():
            return ""
        
        with open(config_path, 'r', encoding='utf-8') as f:
            config = json.load(f)
        
        shared_secret = config.get('shared_secret', '')
        if not shared_secret:
            return ""
        
        # Steam TOTP算法
        return self._generate_steam_totp(shared_secret)
    
    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
        secret_bytes = base64.b64decode(shared_secret)
        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交易"""
        try:
            result = subprocess.run(
                [self.sda_path, '--confirm-trade', trade_id, '--user', username],
                capture_output=True,
                text=True,
                timeout=10
            )
            return result.returncode == 0
        except Exception as e:
            print(f"交易确认失败: {e}")
            return False

许可证验证

# core/auth/license.py
import hashlib
import hmac
import requests
import platform
import subprocess
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.backends import default_backend

class LicenseValidator:
    def __init__(self):
        self.api_url = "https://your-api.com/validate"
        self.public_key = self._load_public_key()
    
    def get_hardware_id(self) -> str:
        """获取硬件ID"""
        components = []
        
        # CPU ID
        try:
            result = subprocess.run(
                ['wmic', 'cpu', 'get', 'ProcessorId'],
                capture_output=True,
                text=True
            )
            cpu_id = result.stdout.strip().split('\n')[1].strip()
            components.append(cpu_id)
        except:
            components.append(platform.processor())
        
        # 主板序列号
        try:
            result = subprocess.run(
                ['wmic', 'baseboard', 'get', 'SerialNumber'],
                capture_output=True,
                text=True
            )
            board_id = result.stdout.strip().split('\n')[1].strip()
            components.append(board_id)
        except:
            components.append(platform.machine())
        
        # MAC地址
        import uuid
        mac = ':'.join(['{:02x}'.format((uuid.getnode() >> elements) & 0xff) 
                        for elements in range(0,2*6,2)][::-1])
        components.append(mac)
        
        # 生成硬件ID
        combined = '|'.join(components)
        hwid = hashlib.sha256(combined.encode()).hexdigest()
        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):
                    return True
        except Exception as e:
            print(f"在线验证失败: {e}")
        
        return False
    
    def _verify_signature(self, data: dict) -> bool:
        """验证RSA签名"""
        try:
            message = data['data'].encode()
            signature = base64.b64decode(data['signature'])
            
            self.public_key.verify(
                signature,
                message,
                padding.PSS(
                    mgf=padding.MGF1(hashes.SHA256()),
                    salt_length=padding.PSS.MAX_LENGTH
                ),
                hashes.SHA256()
            )
            return True
        except:
            return False
    
    def _load_public_key(self):
        """加载RSA公钥"""
        # 从资源文件或代码中加载公钥
        public_key_pem = """
        -----BEGIN PUBLIC KEY-----
        MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
        -----END PUBLIC KEY-----
        """
        # 解析并返回公钥对象
        # ... 实现细节
        pass

四、关键差异和影响

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

六、打包方案

6.1 使用PyInstaller

# requirements.txt
PyQt6==6.6.0
playwright==1.40.0
cryptography==41.0.7
requests==2.31.0

# setup.py
from PyInstaller.utils.hooks import collect_data_files

datas = collect_data_files('PyQt6')
datas += [('resources', 'resources')]

# 打包命令
pyinstaller --onefile \
    --windowed \
    --name="BuffApp" \
    --icon="resources/icon.ico" \
    --add-data="resources;resources" \
    --hidden-import=PyQt6 \
    --hidden-import=playwright \
    main.py

6.2 打包Chrome浏览器

# 在PyInstaller spec文件中配置
a = Analysis(
    ['main.py'],
    pathex=[],
    binaries=[],
    datas=[
        ('resources/chromium', 'resources/chromium'),
        ('resources/extensions', 'resources/extensions'),
        ('resources/sda', 'resources/sda'),
    ],
    hiddenimports=['PyQt6', 'playwright'],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=[],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)

七、性能对比

操作 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++,功能无影响,开发效率更高!