feat: 完成T-205账号就绪引导保护

CollectWorker 增加采集前账号就绪预检:无账号、匹配账号 CDP 端口未响应或未登录时返回 blocked,不进入逐条采集,也不写 skipped/failed。

CollectTab 收到 blocked 后弹窗汇总原因,并跳转/引导到④账号管理配置账号、启动对应 Chrome、人工登录;MainWindow 为①传入打开④的回调。

新增 GUI 测试覆盖无账号、Chrome 未启动、未登录和弹窗引导;同步 T-205 状态、api、current-state 与 progress。
This commit is contained in:
chengma
2026-06-27 15:00:15 +08:00
parent d7e609434e
commit 5390debf40
6 changed files with 281 additions and 15 deletions
+119 -4
View File
@@ -75,7 +75,7 @@ QTabBar::tab:hover:!selected {
if QT_IMPORT_ERROR is None:
from . import accounts, appconfig, db, editor, excel
from . import accounts, appconfig, chrome, db, editor, excel
from . import config as account_config
@@ -196,11 +196,19 @@ if QT_IMPORT_ERROR is None:
class CollectTab(QWidget):
"""Tab 1: import Excel files and list imported tasks."""
def __init__(self, parent=None, db_path=None, config=None, status_callback=None):
def __init__(
self,
parent=None,
db_path=None,
config=None,
status_callback=None,
open_accounts_callback=None,
):
super().__init__(parent)
self.config = appconfig.load_config() if config is None else config
self.db_path = _database_path(db_path, self.config)
self.status_callback = status_callback
self.open_accounts_callback = open_accounts_callback
self.current_batch_id = None
self.has_import_result = False
self.last_import_stats = None
@@ -235,8 +243,8 @@ if QT_IMPORT_ERROR is None:
summary_layout.addWidget(self.show_all_button)
summary_layout.addWidget(self.show_unmatched_button)
self.model = TaskTableModel(self)
self.table = QTableView()
self.model = TaskTableModel(self.table)
self.table.setModel(self.model)
self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
self.table.setSelectionMode(QAbstractItemView.SingleSelection)
@@ -272,6 +280,16 @@ if QT_IMPORT_ERROR is None:
QMessageBox.warning(self, "导入采集", str(message))
self._set_status(str(message))
def _show_account_guide(self, message):
full_message = (
f"{message}\n\n"
"请先到「④ 账号管理」配置账号、启动对应账号 Chrome,并确认已人工登录 Shopee。"
)
QMessageBox.warning(self, "账号未就绪", full_message)
self._set_status(full_message.replace("\n", " "))
if self.open_accounts_callback is not None:
self.open_accounts_callback()
def _choose_excel_files(self):
files, _selected_filter = QFileDialog.getOpenFileNames(
self,
@@ -433,6 +451,9 @@ if QT_IMPORT_ERROR is None:
def _on_collect_finished(self, payload):
self._set_collect_running(False)
self.refresh_tasks()
if payload.get("blocked"):
self._show_collect_blocked(payload)
return
message = "采集完成:成功{collected},略过{skipped},失败{failed}".format(
collected=payload.get("collected", 0),
skipped=payload.get("skipped", 0),
@@ -450,6 +471,36 @@ if QT_IMPORT_ERROR is None:
return
self._set_status(message)
def _show_collect_blocked(self, payload):
lines = ["采集前检查未通过。"]
if payload.get("no_accounts"):
lines.append("当前没有配置账号。")
not_running = payload.get("not_running") or []
if not_running:
lines.append(
"以下账号 Chrome 未启动或调试端口不可访问:"
+ "、".join(self._account_label(item) for item in not_running)
)
logged_out = payload.get("logged_out") or []
if logged_out:
lines.append(
"以下账号未登录 Shopee:"
+ "、".join(self._account_label(item) for item in logged_out)
)
self._show_account_guide("\n".join(lines))
def _account_label(self, item):
if isinstance(item, dict):
name = item.get("account_name") or item.get("alias") or ""
alias = item.get("alias") or ""
reason = item.get("reason")
else:
name = getattr(item, "account_name", "") or getattr(item, "alias", "")
alias = getattr(item, "alias", "")
reason = getattr(item, "reason", None)
label = f"{name}({alias})" if alias and name != alias else (name or alias)
return f"{label}: {reason}" if reason else label
def _on_collect_cancelled(self, payload):
self._set_collect_running(False)
self.refresh_tasks()
@@ -641,11 +692,12 @@ if QT_IMPORT_ERROR is None:
class CollectWorker(BaseWorker):
"""Collect old title and cover for imported tasks."""
def __init__(self, tasks, db_path=None, config=None):
def __init__(self, tasks, db_path=None, config=None, preflight=True):
super().__init__()
self.tasks = list(tasks)
self.db_path = db_path
self.config = config
self.preflight = preflight
def execute(self):
account_rows = accounts.list_accounts(path=self.db_path, config=self.config)
@@ -664,6 +716,22 @@ if QT_IMPORT_ERROR is None:
failed = 0
done = 0
if self.preflight:
blocked = self._preflight_block(eligible, account_rows, account_by_alias)
if blocked:
blocked.update(
{
"ok": False,
"blocked": True,
"total": total,
"done": 0,
"collected": 0,
"skipped": 0,
"failed": 0,
}
)
return blocked
for task in eligible:
if self.should_cancel():
break
@@ -732,6 +800,49 @@ if QT_IMPORT_ERROR is None:
"failed": failed,
}
def _preflight_block(self, eligible, account_rows, account_by_alias):
if not account_rows:
return {
"reason": "NO_ACCOUNTS",
"no_accounts": True,
}
required_accounts = []
seen_aliases = set()
for task in eligible:
alias = str(task.alias).strip()
account = account_by_alias.get(alias)
if account is not None and alias not in seen_aliases:
required_accounts.append(account)
seen_aliases.add(alias)
not_running = []
logged_out = []
for account in required_accounts:
if not chrome.is_running(account.debug_port):
not_running.append(self._account_payload(account, "CDP 端口未响应"))
continue
status = self._login_status(account)
if not status.get("logged_in"):
logged_out.append(
self._account_payload(account, self._login_skip_reason(status))
)
if not_running or logged_out:
return {
"reason": "ACCOUNT_NOT_READY",
"not_running": not_running,
"logged_out": logged_out,
}
return None
def _account_payload(self, account, reason=None):
payload = {
"account_name": account.account_name,
"alias": account.alias,
"debug_port": account.debug_port,
}
if reason:
payload["reason"] = reason
return payload
def _emit_progress(self, done, total, collected, skipped, failed):
self.progress.emit(
{
@@ -1088,6 +1199,7 @@ if QT_IMPORT_ERROR is None:
db_path=self.db_path,
config=self.config,
status_callback=self.statusBar().showMessage,
open_accounts_callback=lambda: self.open_accounts_tab(),
)
if title == "④ 账号管理":
return AccountsTab(
@@ -1104,6 +1216,9 @@ if QT_IMPORT_ERROR is None:
def _on_tab_changed(self, index):
self.statusBar().showMessage(f"当前:{self.tabs.tabText(index)}")
def open_accounts_tab(self):
self.tabs.setCurrentIndex(TAB_TITLES.index("④ 账号管理"))
else:
class MainWindow(QMainWindow):
def __init__(self):