feat: 内置并统一使用项目 ADB (#149)

This commit is contained in:
chengma
2026-08-11 12:01:07 +08:00
parent 56b1a46c5c
commit 8f2037db2f
21 changed files with 355 additions and 46 deletions
+12 -2
View File
@@ -108,6 +108,13 @@ VSVersionInfo(
$distPath = Join-Path $buildRoot "dist"
$workPath = Join-Path $buildRoot "work"
$specPath = Join-Path $buildRoot "spec"
$bundledAdbSource = Join-Path $clientRoot "vendor\android-platform-tools\windows"
foreach ($adbFileName in @("adb.exe", "AdbWinApi.dll", "AdbWinUsbApi.dll")) {
$adbSourceFile = Join-Path $bundledAdbSource $adbFileName
if (-not (Test-Path -LiteralPath $adbSourceFile -PathType Leaf)) {
throw "A required bundled ADB source file is missing: $adbSourceFile"
}
}
Write-Host "[4/7] Building the main application..."
Invoke-Checked -FailureMessage "Failed to build the main application" -Command {
@@ -118,6 +125,7 @@ VSVersionInfo(
--collect-all qfluentwidgets `
--collect-all uiautomator2 `
--collect-all adbutils `
--add-binary "$bundledAdbSource;vendor/android-platform-tools/windows" `
(Join-Path $clientRoot "buyer_main.py")
}
@@ -131,11 +139,13 @@ VSVersionInfo(
$mainDist = Join-Path $distPath "CMAutoBuy"
$launcherExe = Join-Path $distPath "Launcher.exe"
$adbExe = Join-Path $mainDist "dependencies\adbutils\binaries\adb.exe"
$bundledAdbDist = Join-Path $mainDist "dependencies\vendor\android-platform-tools\windows"
foreach ($requiredPath in @(
(Join-Path $mainDist "CMAutoBuy.exe"),
(Join-Path $mainDist "dependencies\PyQt5\Qt5\plugins\platforms\qwindows.dll"),
$adbExe,
(Join-Path $bundledAdbDist "adb.exe"),
(Join-Path $bundledAdbDist "AdbWinApi.dll"),
(Join-Path $bundledAdbDist "AdbWinUsbApi.dll"),
$launcherExe
)) {
if (-not (Test-Path -LiteralPath $requiredPath -PathType Leaf)) {
+55
View File
@@ -0,0 +1,55 @@
"""项目内置 ADB 的路径、完整性检查和 adbutils 环境配置。"""
import os
from pathlib import Path
from typing import Tuple
from .db import app_dir
ADB_RELATIVE_DIRECTORY = Path("vendor/android-platform-tools/windows")
REQUIRED_ADB_FILES: Tuple[str, ...] = (
"adb.exe",
"AdbWinApi.dll",
"AdbWinUsbApi.dll",
)
class BundledAdbError(RuntimeError):
"""项目内置 ADB 缺失或不完整。"""
def bundled_adb_directory() -> Path:
"""返回项目内置 ADB 目录,不创建目录。"""
return app_dir() / ADB_RELATIVE_DIRECTORY
def validate_bundled_adb() -> Path:
"""检查 ADB 和两个配套 DLL,完整时返回 adb.exe 绝对路径。"""
directory = bundled_adb_directory()
missing_files = [
name for name in REQUIRED_ADB_FILES
if not (directory / name).is_file()
]
if missing_files:
names = "、".join(missing_files)
raise BundledAdbError(
f"内置 ADB 文件缺失:{names}。请重新安装完整的软件包。"
)
return (directory / "adb.exe").resolve()
def bundled_adb_path() -> Path:
"""返回预期的项目内 adb.exe 路径;不访问系统 PATH。"""
return (bundled_adb_directory() / "adb.exe").resolve()
def configure_bundled_adb_environment() -> Path:
"""校验内置 ADB,并强制 adbutils/uiautomator2 使用同一文件。"""
adb_path = validate_bundled_adb()
os.environ["ADBUTILS_ADB_PATH"] = str(adb_path)
return adb_path
+43 -9
View File
@@ -4,8 +4,15 @@ from dataclasses import dataclass
import ipaddress
import subprocess
import time
from pathlib import Path
from typing import Callable, List, Optional, Sequence
from .adb_runtime import (
BundledAdbError,
bundled_adb_path,
validate_bundled_adb,
)
DEFAULT_ADB_TIMEOUT_SECONDS = 5.0
DEFAULT_DEVICE_READY_CHECKS = 8
@@ -78,19 +85,24 @@ class AndroidDeviceService:
command_runner: CommandRunner = run_adb_command,
timeout_seconds: float = DEFAULT_ADB_TIMEOUT_SECONDS,
sleeper: Sleeper = time.sleep,
adb_executable: Optional[Path] = None,
):
if timeout_seconds <= 0:
raise ValueError("ADB 超时时间必须大于 0")
self._run_command = command_runner
self._timeout_seconds = timeout_seconds
self._sleep = sleeper
self._uses_bundled_adb = adb_executable is None
self._adb_executable = str(adb_executable or bundled_adb_path())
def search(
self, is_cancelled: Optional[Callable[[], bool]] = None
) -> List[AndroidDevice]:
"""返回当前 ADB 设备;取消后停止补充设备属性。"""
result = self._run(["adb", "devices", "-l"], "搜索设备")
result = self._run(
[self._adb_executable, "devices", "-l"], "搜索设备"
)
devices = self.parse_devices(result.stdout or "")
enriched = []
@@ -158,7 +170,15 @@ class AndroidDeviceService:
raise AndroidDeviceSearchError("应用包名格式不正确")
result = self._run(
["adb", "-s", serial, "shell", "pm", "path", package_name],
[
self._adb_executable,
"-s",
serial,
"shell",
"pm",
"path",
package_name,
],
"检测 PDD 应用",
)
return any(
@@ -193,7 +213,7 @@ class AndroidDeviceService:
self._check_cancelled(is_cancelled)
self._report(on_progress, "正在读取勾选设备的 Wi-Fi 地址…")
route = self._run(
["adb", "-s", usb_serial, "shell", "ip", "route"],
[self._adb_executable, "-s", usb_serial, "shell", "ip", "route"],
"读取 Wi-Fi 地址",
)
ip_address = self.parse_wifi_ipv4(route.stdout or "")
@@ -202,7 +222,7 @@ class AndroidDeviceService:
self._check_cancelled(is_cancelled)
self._report(on_progress, "正在开启 ADB 5555 端口…")
self._run(
["adb", "-s", usb_serial, "tcpip", "5555"],
[self._adb_executable, "-s", usb_serial, "tcpip", "5555"],
"开启 Wi-Fi 调试",
)
@@ -312,7 +332,14 @@ class AndroidDeviceService:
try:
result = self._run(
["adb", "-s", serial, "shell", "getprop", property_name],
[
self._adb_executable,
"-s",
serial,
"shell",
"getprop",
property_name,
],
"读取设备信息",
)
except AndroidDeviceSearchError:
@@ -343,12 +370,14 @@ class AndroidDeviceService:
self._sleep(DEFAULT_DEVICE_READY_INTERVAL_SECONDS)
def _list_devices(self) -> List[AndroidDevice]:
result = self._run(["adb", "devices", "-l"], "读取设备列表")
result = self._run(
[self._adb_executable, "devices", "-l"], "读取设备列表"
)
return self.parse_devices(result.stdout or "")
def _connect_wifi(self, wifi_serial: str) -> None:
result = self._run(
["adb", "connect", wifi_serial],
[self._adb_executable, "connect", wifi_serial],
"连接 Wi-Fi 设备",
)
detail = " ".join(
@@ -401,11 +430,16 @@ class AndroidDeviceService:
callback(message)
def _run(self, command: Sequence[str], action: str):
if self._uses_bundled_adb:
try:
validate_bundled_adb()
except BundledAdbError as exc:
raise AndroidDeviceSearchError(str(exc)) from exc
try:
result = self._run_command(command, self._timeout_seconds)
except FileNotFoundError as exc:
raise AndroidDeviceSearchError(
"未找到 adb,请安装 Android platform-tools 并把 adb 加入 PATH 后重试"
"内置 adb.exe 无法启动,请重新安装完整的软件包"
) from exc
except subprocess.TimeoutExpired as exc:
raise AndroidDeviceSearchError(
@@ -413,7 +447,7 @@ class AndroidDeviceService:
) from exc
except OSError as exc:
raise AndroidDeviceSearchError(
f"无法启动 adb:{str(exc) or '系统拒绝执行'}"
f"无法启动内置 adb:{str(exc) or '系统拒绝执行'}"
) from exc
if result.returncode != 0:
+8
View File
@@ -34,6 +34,14 @@ def data_dir() -> Path:
return directory
def app_dir() -> Path:
"""返回只读资源根目录;源码和打包运行使用同一相对结构。"""
if getattr(sys, "frozen", False):
return Path(sys._MEIPASS).resolve()
return Path(__file__).resolve().parents[1]
def default_database_path() -> Path:
"""返回默认数据库文件路径。"""
+5
View File
@@ -12,6 +12,7 @@ from contextlib import AbstractContextManager, contextmanager
from contextvars import ContextVar
from typing import Any, Callable, Iterator, Optional
from .adb_runtime import BundledAdbError, configure_bundled_adb_environment
from .performance_timing import current_performance_trace
@@ -80,6 +81,10 @@ class PddDeviceService:
@staticmethod
def _default_connector(serial: str) -> Any:
try:
configure_bundled_adb_environment()
except BundledAdbError as exc:
raise PddDeviceError("DEVICE_ADB_MISSING", str(exc)) from exc
try:
import uiautomator2 as u2
except ImportError as exc:
+14 -2
View File
@@ -15,6 +15,7 @@ from PyQt5.QtCore import (
pyqtSlot,
)
from .adb_runtime import BundledAdbError, validate_bundled_adb
from .android_device_service import (
AndroidDeviceConversionCancelled,
AndroidDeviceSearchError,
@@ -359,6 +360,12 @@ class SettingsPageEventBinder(QObject):
self._live_purchase_adapter_ready = bool(
live_purchase_adapter_ready
)
self._adb_unavailable_message = ""
if android_device_service is None:
try:
validate_bundled_adb()
except BundledAdbError as exc:
self._adb_unavailable_message = str(exc)
self._android_device_service = (
android_device_service or AndroidDeviceService()
)
@@ -416,8 +423,12 @@ class SettingsPageEventBinder(QObject):
self._load_current_client()
self._load_selected_android_device()
if self._adb_unavailable_message:
self._page.deviceStatusLabel.setText(
self._adb_unavailable_message
)
self._sync_button_state()
if self._saved_android_serial:
if self._saved_android_serial and not self._adb_unavailable_message:
QTimer.singleShot(0, self._request_restore_saved_android_device)
@pyqtSlot()
@@ -910,7 +921,8 @@ class SettingsPageEventBinder(QObject):
and not self._android_setting_busy
)
device_commands_enabled = (
not self._busy
not self._adb_unavailable_message
and not self._busy
and not self._search_busy
and not self._wifi_conversion_busy
and not self._android_setting_busy
+24 -1
View File
@@ -17,16 +17,18 @@
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtWidgets import QApplication
from qfluentwidgets import (
FluentIcon as FIF,
FluentWindow,
MessageBox,
NavigationItemPosition,
Theme,
setTheme,
)
from .adb_runtime import BundledAdbError, configure_bundled_adb_environment
from .pdd_ui import PDDTaskPage
from .pdd_ui_event import PDDTaskPageEvent
from .pdd_u2_purchase_adapter import (
@@ -147,8 +149,19 @@ def ui_main():
app = QApplication(sys.argv)
setTheme(Theme.AUTO)
adb_error = ""
try:
configure_bundled_adb_environment()
except BundledAdbError as exc:
adb_error = str(exc)
window = MainWindow()
window.showMaximized()
if adb_error:
QTimer.singleShot(
0,
lambda: _show_adb_missing_dialog(window, adb_error),
)
try:
mark_current_version_healthy()
except OSError:
@@ -157,5 +170,15 @@ def ui_main():
return app.exec_()
def _show_adb_missing_dialog(parent: MainWindow, message: str) -> None:
"""在主窗口中央提示内置 ADB 缺失,用户可用大按钮关闭。"""
dialog = MessageBox("Android 调试组件缺失", message, parent)
dialog.yesButton.setText("知道了")
dialog.yesButton.setMinimumSize(120, 40)
dialog.cancelButton.hide()
dialog.exec()
if __name__ == "__main__":
sys.exit(ui_main())
+50
View File
@@ -0,0 +1,50 @@
"""项目内置 ADB 路径与完整性检查测试。"""
import os
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from src.adb_runtime import (
BundledAdbError,
REQUIRED_ADB_FILES,
bundled_adb_path,
configure_bundled_adb_environment,
validate_bundled_adb,
)
class BundledAdbRuntimeTest(unittest.TestCase):
def test_project_adb_is_complete(self):
adb_path = validate_bundled_adb()
self.assertEqual(adb_path, bundled_adb_path())
self.assertTrue(adb_path.is_absolute())
for file_name in REQUIRED_ADB_FILES:
self.assertTrue((adb_path.parent / file_name).is_file())
def test_missing_file_reports_name_without_path_fallback(self):
with tempfile.TemporaryDirectory() as directory, patch(
"src.adb_runtime.app_dir", return_value=Path(directory)
):
with self.assertRaisesRegex(BundledAdbError, "adb.exe"):
validate_bundled_adb()
def test_configure_forces_adbutils_to_use_project_adb(self):
old_value = os.environ.get("ADBUTILS_ADB_PATH")
try:
configured = configure_bundled_adb_environment()
self.assertEqual(
os.environ["ADBUTILS_ADB_PATH"],
str(configured),
)
finally:
if old_value is None:
os.environ.pop("ADBUTILS_ADB_PATH", None)
else:
os.environ["ADBUTILS_ADB_PATH"] = old_value
if __name__ == "__main__":
unittest.main()
+23 -19
View File
@@ -3,12 +3,16 @@
import subprocess
import unittest
from src.adb_runtime import bundled_adb_path
from src.android_device_service import (
AndroidDeviceSearchError,
AndroidDeviceService,
)
ADB = str(bundled_adb_path())
def completed(command, stdout="", stderr="", returncode=0):
"""创建一条假的 ADB 命令结果。"""
@@ -56,7 +60,7 @@ USB-002 unauthorized usb:1-2 transport_id:3
def runner(command, _timeout):
commands.append(list(command))
if command == ["adb", "devices", "-l"]:
if command == [ADB, "devices", "-l"]:
return completed(
command,
"List of devices attached\n"
@@ -111,12 +115,12 @@ USB-002 unauthorized usb:1-2 transport_id:3
def runner(command, _timeout):
command = list(command)
commands.append(command)
if command[:2] == ["adb", "connect"]:
if command[:2] == [ADB, "connect"]:
return completed(
command,
"already connected to 192.168.0.173:5555\n",
)
if command == ["adb", "devices", "-l"]:
if command == [ADB, "devices", "-l"]:
return completed(
command,
"List of devices attached\n"
@@ -131,7 +135,7 @@ USB-002 unauthorized usb:1-2 transport_id:3
self.assertEqual(devices[0].serial, "192.168.0.173:5555")
self.assertEqual(
commands[0],
["adb", "connect", "192.168.0.173:5555"],
[ADB, "connect", "192.168.0.173:5555"],
)
def test_restore_saved_wifi_waits_for_authorization_handshake(self):
@@ -141,9 +145,9 @@ USB-002 unauthorized usb:1-2 transport_id:3
def runner(command, _timeout):
nonlocal device_list_count
command = list(command)
if command[:2] == ["adb", "connect"]:
if command[:2] == [ADB, "connect"]:
return completed(command, "connected to 192.168.0.173:5555\n")
if command == ["adb", "devices", "-l"]:
if command == [ADB, "devices", "-l"]:
device_list_count += 1
status = "unauthorized" if device_list_count == 1 else "device"
return completed(
@@ -167,7 +171,7 @@ USB-002 unauthorized usb:1-2 transport_id:3
def runner(command, _timeout):
command = list(command)
commands.append(command)
if command == ["adb", "devices", "-l"]:
if command == [ADB, "devices", "-l"]:
return completed(
command,
"List of devices attached\n"
@@ -181,12 +185,12 @@ USB-002 unauthorized usb:1-2 transport_id:3
self.assertEqual(devices[0].serial, "USB-001")
self.assertFalse(
any(command[:2] == ["adb", "connect"] for command in commands)
any(command[:2] == [ADB, "connect"] for command in commands)
)
def test_missing_model_is_read_from_device(self):
def runner(command, _timeout):
if command == ["adb", "devices", "-l"]:
if command == [ADB, "devices", "-l"]:
return completed(
command,
"List of devices attached\nUSB-001 device transport_id:1\n",
@@ -202,7 +206,7 @@ USB-002 unauthorized usb:1-2 transport_id:3
def test_property_failure_keeps_discovered_device(self):
def runner(command, _timeout):
if command == ["adb", "devices", "-l"]:
if command == [ADB, "devices", "-l"]:
return completed(
command,
"List of devices attached\nUSB-001 device model:Phone\n",
@@ -218,7 +222,7 @@ USB-002 unauthorized usb:1-2 transport_id:3
def runner(_command, _timeout):
raise FileNotFoundError("adb")
with self.assertRaisesRegex(AndroidDeviceSearchError, "未找到 adb"):
with self.assertRaisesRegex(AndroidDeviceSearchError, "内置 adb.exe"):
AndroidDeviceService(runner).search()
def test_timeout_has_recovery_message(self):
@@ -253,7 +257,7 @@ USB-002 unauthorized usb:1-2 transport_id:3
self.assertEqual(
commands,
[[
"adb",
ADB,
"-s",
"USB-001",
"shell",
@@ -296,9 +300,9 @@ USB-002 unauthorized usb:1-2 transport_id:3
"192.168.0.0/24 dev wlan0 scope link "
"src 192.168.0.173\n",
)
if command[:2] == ["adb", "connect"]:
if command[:2] == [ADB, "connect"]:
return completed(command, "connected to 192.168.0.173:5555\n")
if command == ["adb", "devices", "-l"]:
if command == [ADB, "devices", "-l"]:
return completed(
command,
"List of devices attached\n"
@@ -323,14 +327,14 @@ USB-002 unauthorized usb:1-2 transport_id:3
["USB-001", "192.168.0.173:5555"],
)
self.assertIn(
["adb", "-s", "USB-001", "tcpip", "5555"],
[ADB, "-s", "USB-001", "tcpip", "5555"],
commands,
)
self.assertEqual(
commands.count(["adb", "connect", "192.168.0.173:5555"]),
commands.count([ADB, "connect", "192.168.0.173:5555"]),
1,
)
self.assertEqual(commands.count(["adb", "devices", "-l"]), 3)
self.assertEqual(commands.count([ADB, "devices", "-l"]), 3)
self.assertTrue(
any("保存后拔掉 USB" in message for message in progress)
)
@@ -346,9 +350,9 @@ USB-002 unauthorized usb:1-2 transport_id:3
command = list(command)
if command[-3:] == ["shell", "ip", "route"]:
return completed(command, "local src 192.168.0.173\n")
if command[:2] == ["adb", "connect"]:
if command[:2] == [ADB, "connect"]:
return completed(command, "connected to 192.168.0.173:5555\n")
if command == ["adb", "devices", "-l"]:
if command == [ADB, "devices", "-l"]:
return completed(
command,
"List of devices attached\n"
+21 -1
View File
@@ -21,7 +21,7 @@ from build_tools.release_manifest import (
versioned_manifest_file_name,
write_manifest,
)
from src.db import data_dir
from src.db import app_dir, data_dir
class LauncherPathTest(unittest.TestCase):
@@ -139,6 +139,26 @@ class PackagedDataPathTest(unittest.TestCase):
):
self.assertEqual(data_dir(), root / "data")
def test_packaged_resource_path_uses_pyinstaller_directory(self):
with tempfile.TemporaryDirectory() as directory, patch(
"src.db.sys.frozen", True, create=True
), patch("src.db.sys._MEIPASS", directory, create=True):
self.assertEqual(app_dir(), Path(directory).resolve())
class BundledAdbPackagingTest(unittest.TestCase):
def test_build_script_packages_and_checks_all_adb_files(self):
client_root = Path(__file__).resolve().parents[1]
script = (client_root / "packaging" / "build.ps1").read_text(
encoding="utf-8-sig"
)
self.assertIn("--add-binary", script)
self.assertIn("vendor/android-platform-tools/windows", script)
self.assertIn("adb.exe", script)
self.assertIn("AdbWinApi.dll", script)
self.assertIn("AdbWinUsbApi.dll", script)
class ReleaseManifestTest(unittest.TestCase):
def test_manifest_name_and_hash_match_release_files(self):
+35
View File
@@ -14,6 +14,7 @@ from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication
from qfluentwidgets import InfoBarPosition
from src.adb_runtime import BundledAdbError
from src.android_device_service import AndroidDeviceSearchError
from src.db import open_database
from src.pdd_ui import PDDTaskPage
@@ -1174,6 +1175,40 @@ class PDDTaskPageEventTest(unittest.TestCase):
self.assertEqual(result, 0)
self.assertTrue(window.maximized)
def test_application_startup_shows_dialog_when_bundled_adb_is_missing(self):
class FakeApplication:
@staticmethod
def setAttribute(*_args):
pass
def __init__(self, _args):
pass
def exec_(self):
return 0
class FakeWindow:
def showMaximized(self):
pass
window = FakeWindow()
message = "内置 ADB 文件缺失:adb.exe。请重新安装完整的软件包。"
with patch("src.ui_main.QApplication", FakeApplication), patch(
"src.ui_main.MainWindow", return_value=window
), patch("src.ui_main.setTheme"), patch(
"src.ui_main.mark_current_version_healthy"
), patch(
"src.ui_main.configure_bundled_adb_environment",
side_effect=BundledAdbError(message),
), patch(
"src.ui_main.QTimer.singleShot",
side_effect=lambda _delay, callback: callback(),
), patch("src.ui_main._show_adb_missing_dialog") as show_dialog:
result = ui_main()
self.assertEqual(result, 0)
show_dialog.assert_called_once_with(window, message)
def test_main_window_marks_and_clears_update_navigation(self):
window = MainWindow(
task_repository=self.repository,
+20
View File
@@ -5,6 +5,7 @@ import tempfile
import time
import unittest
from pathlib import Path
from unittest.mock import patch
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
@@ -12,6 +13,7 @@ from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtTest import QTest
from PyQt5.QtWidgets import QApplication
from src.adb_runtime import BundledAdbError
from src.android_device_service import (
AndroidDevice,
AndroidDeviceConversionCancelled,
@@ -218,6 +220,24 @@ class SettingsPageEventTest(unittest.TestCase):
page.eventBinder.shutdown()
page.deleteLater()
def test_missing_bundled_adb_disables_device_commands(self):
message = "内置 ADB 文件缺失:adb.exe。请重新安装完整的软件包。"
with patch(
"src.settings_ui_event.validate_bundled_adb",
side_effect=BundledAdbError(message),
):
page = SettingsPage(
settings_repository=self.repository,
admin_gateway=MockAdminGateway(),
)
self.assertEqual(page.deviceStatusLabel.text(), message)
self.assertFalse(page.searchButton.isEnabled())
self.assertFalse(page.convertWifiButton.isEnabled())
self.assertTrue(page.currentDeviceSaveButton.isEnabled())
page.eventBinder.shutdown()
page.deleteLater()
def test_compact_form_layout_keeps_related_fields_on_same_row(self):
page = SettingsPage(
settings_repository=self.repository,
+13 -2
View File
@@ -16,6 +16,8 @@ import xml.etree.ElementTree as ET
from collections.abc import Callable
from typing import Any
from src.adb_runtime import configure_bundled_adb_environment
PDD_PACKAGE_NAME = "com.xunmeng.pinduoduo"
READY_MARKERS = ("发起拼单", "立即购买", "单独购买", "免拼购买", "快要抢光")
@@ -59,11 +61,20 @@ def wait_goods_ready(device: Any, first_xml: str, timeout: float) -> None:
def measure_once(serial: str, goods_url: str, cold: bool) -> dict[str, int]:
adb_executable = str(configure_bundled_adb_environment())
import uiautomator2 as u2
if cold:
subprocess.run(
["adb", "-s", serial, "shell", "am", "force-stop", PDD_PACKAGE_NAME],
[
adb_executable,
"-s",
serial,
"shell",
"am",
"force-stop",
PDD_PACKAGE_NAME,
],
check=True,
capture_output=True,
text=True,
@@ -80,7 +91,7 @@ def measure_once(serial: str, goods_url: str, cold: bool) -> dict[str, int]:
_, result["adb_device_check"] = measure_call(
"adb_device_check",
lambda: subprocess.run(
["adb", "-s", serial, "get-state"],
[adb_executable, "-s", serial, "get-state"],
check=True,
capture_output=True,
text=True,
+4 -2
View File
@@ -26,6 +26,7 @@ from src.pdd_page_classifier import ( # noqa: E402
PAGE_HOME,
classify_pdd_page,
)
from src.adb_runtime import configure_bundled_adb_environment # noqa: E402
TEST_URL = "https://mobile.yangkeduo.com/goods.html"
@@ -143,15 +144,16 @@ def run_diagnostic(
address = str(device_address or "").strip()
if not address:
raise ValueError("必须提供 Android 设备地址")
adb_executable = str(configure_bundled_adb_environment())
adb_runner(
["adb", "-s", address, "get-state"],
[adb_executable, "-s", address, "get-state"],
check=True,
capture_output=True,
text=True,
)
adb_runner(
[
"adb", "-s", address, "shell", "am", "start",
adb_executable, "-s", address, "shell", "am", "start",
"-a", "android.intent.action.VIEW", "-d", TEST_URL,
],
check=True,
Binary file not shown.
Binary file not shown.
+15
View File
@@ -0,0 +1,15 @@
# 内置 ADB 文件说明
- 来源目录:用户指定的 `D:\Portable\adb`
- 复制日期:2026-08-11
- ADB 版本:1.0.32
运行所需文件及 SHA-256:
| 文件 | SHA-256 |
|---|---|
| `adb.exe` | `AD5D27384D1B5BCEB6342BB3204FEE977217422BCAFD252AB18F763A3DE43931` |
| `AdbWinApi.dll` | `14A51482AA003DB79A400F4B15C158397FE6D57EE6606B3D633FA431A7BFDF4B` |
| `AdbWinUsbApi.dll` | `041C6859BB4FC78D3A903DD901298CD1ECFB75B6BE0646B74954CD722280A407` |
升级 ADB 时必须整体替换这三个文件,重新记录版本和哈希,并完成 USB、Wi-Fi 和 uiautomator2 真机验证。
Binary file not shown.
+7 -6
View File
@@ -15,12 +15,12 @@
|---|---|---|
| Python | 3.10 | 打开 PowerShell 执行 `C:/Python310/python.exe --version`,输出 `Python 3.10.x` |
| Git | 任意较新版本 | `git --version` 有输出 |
| adb(安卓调试工具) | 任意较新版本 | `adb version` 有输出 |
| adb(安卓调试工具) | 项目内置 1.0.32 | `./vendor/android-platform-tools/windows/adb.exe version` 有输出 |
说明:
- 项目固定使用 `C:/Python310/python.exe`。**不要用 `python` 这个命令**,因为电脑上可能装了多个 Python,`python` 指向哪一个不确定。本文档所有命令都写全路径。
- adb 一般随 Android SDK Platform Tools 安装。装好后要把它所在目录加入系统环境变量 `Path`,否则 `adb` 命令找不到。
- Client 固定使用 `client/vendor/android-platform-tools/windows/` 中的 ADB,不读取系统环境变量 `Path`。三个内置文件必须同时存在;缺失时程序会弹窗提示重新安装。
## 2. 安装依赖
@@ -50,12 +50,12 @@ C:/Python310/python.exe -m pip freeze | Select-String "xxx"
只做采集/采购的界面开发时**可以跳过这一步**,界面不连手机也能打开。
1. 手机打开「开发者选项」→「USB 调试」。
2. 用数据线连电脑,执行 `adb devices`,能看到一行设备号 + `device`。
2. 用数据线连电脑,在 `client/` 目录执行 `./vendor/android-platform-tools/windows/adb.exe devices`,能看到一行设备号 + `device`。
3. 打开无线调试(这样后面不用一直插着线):
```powershell
adb tcpip 5555
adb connect 192.168.0.173:5555
./vendor/android-platform-tools/windows/adb.exe tcpip 5555
./vendor/android-platform-tools/windows/adb.exe connect 192.168.0.173:5555
```
把 `192.168.0.173` 换成手机的实际 IP(手机「设置 → 关于手机 → 状态信息」里能看到)。
@@ -95,7 +95,8 @@ C:/Python310/python.exe buyer_main.py
| `ModuleNotFoundError: No module named 'qfluentwidgets'` | 依赖没装,或装到了别的 Python 上 | 回到第 2 步,注意用 `C:/Python310/python.exe -m pip` |
| `ImportError: DLL load failed`(导入 PyQt5 时) | 装了多个 Qt 绑定互相冲突 | 卸载干净再装:`pip uninstall PyQt5 PyQt6 PySide2 PySide6 -y`,然后重新执行第 2 步 |
| 窗口一闪就没了 | 直接双击 .py 文件运行的 | 必须在 PowerShell 里跑,才能看到错误信息 |
| `adb: device offline` / `connect failed` | 手机和电脑不在同一个 WiFi,或手机重启过 | 重新执行 `adb connect <手机IP>:5555` |
| `adb: device offline` / `connect failed` | 手机和电脑不在同一个 WiFi,或手机重启过 | 使用项目内 adb 重新执行 `./vendor/android-platform-tools/windows/adb.exe connect <手机IP>:5555` |
| `内置 ADB 文件缺失` | 发布包不完整,或三个 ADB 文件没有一起复制 | 重新安装完整软件包;不要改用系统 PATH 中的 adb |
| 跑 demo 后目录里多出 `xxx_home.xml`、`xxx_home.png` | `src/demo1/auto_v1.py` 会把控件树和截图写到当前目录 | 正常现象,这些是调试产物,**不要提交到 Git** |
界面相关的问题排查不了时,先确认是不是 Qt 主线程被卡住了 —— 见 `02-architecture.md` §5。
+5 -2
View File
@@ -198,7 +198,10 @@ CMAutoBuy/ 整个文件夹拷到任何机器都能用
│ │ ├── bin/ Qt5Core.dll / Qt5Gui.dll / Qt5Widgets.dll …
│ │ └── plugins/platforms/qwindows.dll
│ ├── qfluentwidgets/ qss 样式、字体、图标资源
│ ├── adbutils/binaries/adb.exe
│ ├── vendor/android-platform-tools/windows/
│ │ ├── adb.exe
│ │ ├── AdbWinApi.dll
│ │ └── AdbWinUsbApi.dll
│ └── …
└── data/ 本地数据,升级时保留(见 [03 数据模型](03-data-model.md) §2.1)
├── client.db
@@ -246,7 +249,7 @@ CMAutoBuy/ 整个文件夹拷到任何机器都能用
|---|---|
| Qt 平台插件没收集到 | 启动即报 `could not find or load the Qt platform plugin "windows"` |
| `qfluentwidgets` 资源没收集到 | 能启动,但界面全白、控件没样式 |
| `adbutils` 的 adb 二进制没收集到 | 界面正常,但连不上手机 |
| 项目内置 ADB 或配套 DLL 没收集到 | 启动时弹窗提示,设备操作保持中断 |
| `uiautomator2` 初始化流程 | 打包后没有 `python -m uiautomator2 init` 这条命令,需确认辅助 App 怎么装 |
| 杀软误报 | 空白版本信息的 exe 最容易被拦。必须给 exe 加图标和版本信息(产品名、版本号)。根治需要代码签名证书 |
| 装到 `C:\Program Files\` | 无写权限,数据被重定向到 VirtualStore,设置改了不生效 |
+1
View File
@@ -299,6 +299,7 @@ class TaskTableModel(QAbstractTableModel):
### Android 设备
- Client 只使用发布包内置的 ADB,不回退系统 `Path`;启动时缺少 `adb.exe` 或配套 DLL,应在主窗口中央弹出带明显关闭按钮的中文提示,并禁用需要 ADB 的设备命令;
- ADB 地址;
- PDD 包名;
- “测试连接”;