fix: 任务执行前检查 Android 设备连接 (#84)

This commit is contained in:
chengma
2026-08-10 09:35:46 +08:00
parent dee9f34035
commit e7fb06d7aa
11 changed files with 360 additions and 25 deletions
+112 -15
View File
@@ -20,7 +20,7 @@
只有必须让用户当场做决定时才用模态对话框。
"""
from typing import Dict, Optional
from typing import Callable, Dict, Optional
from PyQt5.QtCore import (
QCoreApplication,
@@ -32,6 +32,10 @@ from PyQt5.QtCore import (
)
from qfluentwidgets import InfoBar, InfoBarPosition, MessageBox, PushButton
from .android_device_service import (
AndroidDeviceSearchError,
AndroidDeviceService,
)
from .admin_gateway import (
AdminGatewayError,
ClientInfo,
@@ -101,6 +105,7 @@ class ClaimTaskWorker(QObject):
localSaveFailed = pyqtSignal(str, str)
retryableFailed = pyqtSignal(str)
failed = pyqtSignal(str)
deviceUnavailable = pyqtSignal(str)
outcome = pyqtSignal(str, str, str)
completed = pyqtSignal()
@@ -114,6 +119,7 @@ class ClaimTaskWorker(QObject):
purchase_adapter_factory: Optional[PurchaseAdapterFactory] = None,
purchase_reconcile_factory: Optional[PurchaseReconcileFactory] = None,
selected_task_id: str = "",
device_connection_checker: Optional[Callable[[str], None]] = None,
) -> None:
super().__init__()
self._gateway = gateway
@@ -125,6 +131,10 @@ class ClaimTaskWorker(QObject):
self._purchase_adapter_factory = purchase_adapter_factory
self._purchase_reconcile_factory = purchase_reconcile_factory
self._selected_task_id = selected_task_id
self._device_connection_checker = (
device_connection_checker
or AndroidDeviceService().require_connected
)
def cancel(self) -> None:
"""阻止尚未开始的领取;已领取的任务仍必须保存到本地。"""
@@ -147,6 +157,10 @@ class ClaimTaskWorker(QObject):
client_settings.client_name,
)
if self._selected_task_id:
self._device_connection_checker(android_serial or "")
self._task_repository.prepare_collect_rerun(
self._selected_task_id
)
service = CollectTaskService(
self._gateway,
self._task_repository,
@@ -166,10 +180,14 @@ class ClaimTaskWorker(QObject):
purchase_adapter_factory=self._purchase_adapter_factory,
purchase_reconcile_factory=self._purchase_reconcile_factory,
cancelled=lambda: self._cancelled,
device_connection_checker=self._device_connection_checker,
)
result = dispatcher.execute_one()
if not self._cancelled or result.kind == "cancelled":
self.outcome.emit(result.kind, result.message, result.task_id)
except AndroidDeviceSearchError as exc:
if not self._cancelled:
self.deviceUnavailable.emit(str(exc))
except AdminGatewayError as exc:
if not self._cancelled:
request_hint = (
@@ -200,6 +218,7 @@ class PDDTaskPageEvent(QObject):
collect_service_factory: Optional[CollectServiceFactory] = None,
purchase_adapter_factory: Optional[PurchaseAdapterFactory] = None,
purchase_reconcile_factory: Optional[PurchaseReconcileFactory] = None,
device_connection_checker: Optional[Callable[[str], None]] = None,
next_task_delay_ms: int = 500,
no_task_delay_ms: int = 5_000,
retry_delays_ms: tuple[int, ...] = (5_000, 10_000, 20_000, 30_000),
@@ -214,6 +233,7 @@ class PDDTaskPageEvent(QObject):
self._claim_worker: Optional[ClaimTaskWorker] = None
self._rerun_cancel_requested = False
self._rerun_feedback: Optional[InfoBar] = None
self._device_feedback: Optional[InfoBar] = None
self._auto_fetch_running = False
self._stop_requested = False
self._cycle_next_delay_ms: Optional[int] = None
@@ -223,6 +243,10 @@ class PDDTaskPageEvent(QObject):
self._collect_service_factory = collect_service_factory
self._purchase_adapter_factory = purchase_adapter_factory
self._purchase_reconcile_factory = purchase_reconcile_factory
self._device_connection_checker = (
device_connection_checker
or AndroidDeviceService().require_connected
)
if next_task_delay_ms < 0 or no_task_delay_ms <= 0:
raise ValueError("自动获取等待时间配置无效")
if not retry_delays_ms or any(value <= 0 for value in retry_delays_ms):
@@ -325,8 +349,8 @@ class PDDTaskPageEvent(QObject):
)
return
if not android_serial:
self._show_rerun_warning(
"重新执行不可用", "请先在设置页选择并保存 Android 设备。"
self._show_device_unavailable(
"请先在设置页选择并保存 Android 设备"
)
return
try:
@@ -353,17 +377,6 @@ class PDDTaskPageEvent(QObject):
if not dialog.exec():
return
try:
self._repository.prepare_collect_rerun(task_id)
except (CollectRerunError, ValueError) as exc:
self._show_rerun_warning("不能重新执行", str(exc))
self._reload()
return
except Exception:
self._show_rerun_warning(
"重新执行失败", "无法更新本地任务状态,请检查数据库后重试。"
)
return
self._start_rerun_worker(task_id)
def _start_rerun_worker(self, task_id: str) -> None:
@@ -371,10 +384,13 @@ class PDDTaskPageEvent(QObject):
assert self._claim_gateway is not None
self._close_rerun_feedback()
self._close_device_feedback()
self._rerun_cancel_requested = False
self._claim_busy = True
self._page.set_rerun_running(True)
self._page.set_engine_status(f"正在重新采集任务 {task_id}…")
self._page.set_engine_status(
f"正在检查 Android 设备并准备重新采集任务 {task_id}…"
)
self._reload()
thread = QThread(self)
@@ -385,11 +401,13 @@ class PDDTaskPageEvent(QObject):
self._selected_android_device_service,
self._collect_service_factory,
selected_task_id=task_id,
device_connection_checker=self._device_connection_checker,
)
worker.moveToThread(thread)
thread.started.connect(worker.run)
worker.retryableFailed.connect(self._on_rerun_failed)
worker.failed.connect(self._on_rerun_failed)
worker.deviceUnavailable.connect(self._on_rerun_device_unavailable)
worker.outcome.connect(self._on_rerun_outcome)
worker.completed.connect(thread.quit)
worker.completed.connect(worker.deleteLater)
@@ -443,6 +461,16 @@ class PDDTaskPageEvent(QObject):
self._page.set_engine_status(message)
self._show_rerun_error("重新采集失败", message)
@pyqtSlot(str)
def _on_rerun_device_unavailable(self, message: str) -> None:
if self._closing or self._rerun_cancel_requested:
return
content = message or "Android 设备未连接,任务未开始"
self._page.set_engine_status(
f"Android 设备不可用,重新采集未开始:{content}"
)
self._show_device_unavailable(content)
@pyqtSlot()
def _on_rerun_thread_finished(self) -> None:
cancel_requested = self._rerun_cancel_requested
@@ -528,6 +556,7 @@ class PDDTaskPageEvent(QObject):
self._show_claim_error("领取任务失败", message)
return
self._close_device_feedback()
self._auto_fetch_running = True
self._stop_requested = False
self._retry_count = 0
@@ -560,6 +589,7 @@ class PDDTaskPageEvent(QObject):
collect_service_factory=self._collect_service_factory,
purchase_adapter_factory=self._purchase_adapter_factory,
purchase_reconcile_factory=self._purchase_reconcile_factory,
device_connection_checker=self._device_connection_checker,
)
worker.moveToThread(thread)
thread.started.connect(worker.run)
@@ -569,6 +599,7 @@ class PDDTaskPageEvent(QObject):
worker.localSaveFailed.connect(self._on_claimed_task_save_failed)
worker.retryableFailed.connect(self._on_claim_retryable_failed)
worker.failed.connect(self._on_claim_failed)
worker.deviceUnavailable.connect(self._on_claim_device_unavailable)
worker.outcome.connect(self._on_collect_outcome)
worker.completed.connect(thread.quit)
worker.completed.connect(worker.deleteLater)
@@ -682,6 +713,16 @@ class PDDTaskPageEvent(QObject):
self._show_claim_error("领取任务失败", content)
self._stop_after_current(content)
@pyqtSlot(str)
def _on_claim_device_unavailable(self, message: str) -> None:
if self._closing:
return
content = message or "Android 设备未连接,任务未开始"
status = f"自动获取:已停止 · Android 设备不可用:{content}"
self._page.set_engine_status(status)
self._show_device_unavailable(content)
self._stop_after_current(status)
@pyqtSlot(str, str, str)
def _on_collect_outcome(self, kind: str, message: str, task_id: str) -> None:
if self._closing:
@@ -753,6 +794,53 @@ class PDDTaskPageEvent(QObject):
parent=self._page,
)
def _show_device_unavailable(self, content: str) -> None:
"""显示一条可进入设置且不会堆叠的设备错误。"""
self._close_device_feedback()
bar = InfoBar.error(
title="Android 设备不可用",
content=content,
isClosable=True,
duration=-1,
position=InfoBarPosition.TOP_RIGHT,
parent=self._page,
)
self._device_feedback = bar
settings_button = PushButton("打开设置", bar)
settings_button.setAccessibleName("打开 Android 设备设置")
settings_button.clicked.connect(
lambda _checked=False, current=bar: self._open_device_settings(current)
)
close_button = PushButton("关闭提示", bar)
close_button.setAccessibleName("关闭 Android 设备提示")
close_button.clicked.connect(
lambda _checked=False, current=bar: self._close_device_feedback(current)
)
bar.addWidget(settings_button)
bar.addWidget(close_button)
bar.destroyed.connect(
lambda _object=None, current=bar: self._forget_device_feedback(current)
)
def _open_device_settings(self, bar: InfoBar) -> None:
self._page.openSettingsRequested.emit()
self._close_device_feedback(bar)
def _close_device_feedback(self, expected: Optional[InfoBar] = None) -> None:
bar = self._device_feedback
if bar is None or (expected is not None and bar is not expected):
return
self._device_feedback = None
try:
bar.close()
except RuntimeError:
pass
def _forget_device_feedback(self, bar: InfoBar) -> None:
if self._device_feedback is bar:
self._device_feedback = None
@pyqtSlot()
def _on_claim_thread_finished(self) -> None:
self._claim_worker = None
@@ -849,6 +937,7 @@ class PDDTaskPageEvent(QObject):
self._stop_requested = True
self._next_cycle_timer.stop()
self._close_rerun_feedback()
self._close_device_feedback()
for window in list(self._detail_windows.values()):
window.close()
@@ -868,6 +957,14 @@ class PDDTaskPageEvent(QObject):
self._on_claimed_task_save_failed,
),
(worker.failed, self._on_claim_failed),
(
worker.deviceUnavailable,
self._on_claim_device_unavailable,
),
(
worker.deviceUnavailable,
self._on_rerun_device_unavailable,
),
(
worker.retryableFailed,
self._on_claim_retryable_failed,