| 功能模块 | 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完全可以实现所有需求!
| 技术组件 | 选型 | 说明 |
|---|---|---|
| 桌面框架 | PyQt6 / PySide6 | Qt的Python绑定,功能与C++版完全一致 |
| 编程语言 | Python 3.10+ | 开发效率高,代码简洁 |
| 浏览器自动化 | Selenium / Playwright | 比QWebEngine更强大的自动化能力 |
| Web界面 | QWebEngineView | 支持HTML/React界面 |
| 加密库 | cryptography / pycryptodome | Python加密库,功能完整 |
| 打包工具 | PyInstaller / cx_Freeze | 打包为exe |
| 代码保护 | PyArmor / Nuitka | 代码混淆和编译保护 |
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/ # 构建输出
# 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()
# 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/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
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
| 方面 | Python优势 |
|---|---|
| 开发效率 | ⭐⭐⭐⭐⭐ 代码简洁,开发速度快 |
| 浏览器自动化 | ⭐⭐⭐⭐⭐ Selenium/Playwright生态成熟,功能强大 |
| 第三方库 | ⭐⭐⭐⭐⭐ PyPI库丰富,Steam API、加密等都有现成库 |
| 调试便利 | ⭐⭐⭐⭐⭐ Python调试工具完善,开发体验好 |
| 学习曲线 | ⭐⭐⭐⭐⭐ Python语法简单,团队上手快 |
| 方面 | Python劣势 | 影响程度 |
|---|---|---|
| 安全性 | ⭐⭐ Python容易被反编译 | ⚠️ 中等影响(需用PyArmor/Nuitka保护) |
| 性能 | ⭐⭐⭐ 比C++慢,但桌面应用足够 | ✅ 影响较小 |
| 打包体积 | ⭐⭐⭐ 包含Python运行时,体积较大 | ⚠️ 轻微影响(约50-100MB) |
| 启动速度 | ⭐⭐⭐ 比C++慢,但可接受 | ✅ 影响较小 |
| Chrome插件加载 | ⭐⭐⭐ 需要额外处理 | ⚠️ 轻微影响(Playwright支持) |
# 安装
pip install pyarmor
# 混淆保护
pyarmor gen --enable-rft main.py
# 打包
pyinstaller --onefile --key=your-key main.py
优点:
缺点:
# 安装
pip install nuitka
# 编译为exe(会先编译为C++再编译为二进制)
python -m nuitka --onefile --windows-icon=icon.ico main.py
优点:
缺点:
# 1. 核心敏感代码用Nuitka编译为扩展模块
# core/auth/license_core.py -> license_core.pyd
# 2. 其他代码用PyArmor保护
pyarmor gen --enable-rft src/
# 3. 打包
pyinstaller --onefile main.py
# 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
# 在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 |
结论:性能差异在桌面应用中可接受,用户体验无明显影响。
PyQt6 / PySide6 # 桌面框架
Playwright # 浏览器自动化(比Selenium更现代)
cryptography # 加密库
requests # HTTP请求
PyInstaller # 打包工具
Nuitka # 核心模块编译保护
PyArmor # 代码混淆
结论:Python完全可以替代C++,功能无影响,开发效率更高!