feat(collect): show per-item collection activity timer
This commit is contained in:
+218
-1
@@ -20,6 +20,35 @@ def CollectWorker(*args, **kwargs):
|
||||
def WriteBackWorker(*args, **kwargs):
|
||||
return _call_package_attr("WriteBackWorker", _RealWriteBackWorker, *args, **kwargs)
|
||||
|
||||
|
||||
COLLECT_ACTIVITY_STEP_LABELS = {
|
||||
"preflight": "检查账号",
|
||||
"match_account": "匹配账号",
|
||||
"check_login": "检查账号登录",
|
||||
"prepare_task": "准备采集",
|
||||
"open_product": "打开商品页",
|
||||
"wait_ready": "等待商品页加载",
|
||||
"read_title": "读取标题",
|
||||
"read_cover": "读取封面",
|
||||
"download_cover": "下载封面",
|
||||
"save_result": "保存采集结果",
|
||||
"db_write": "保存采集结果",
|
||||
}
|
||||
|
||||
|
||||
def _collect_activity_step_label(step):
|
||||
return COLLECT_ACTIVITY_STEP_LABELS.get(str(step or ""), "处理当前商品")
|
||||
|
||||
|
||||
def _format_collect_elapsed(seconds):
|
||||
total = max(0, int(seconds or 0))
|
||||
hours, remainder = divmod(total, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
if hours:
|
||||
return f"{hours}:{minutes:02d}:{seconds:02d}"
|
||||
return f"{minutes:02d}:{seconds:02d}"
|
||||
|
||||
|
||||
class CollectTab(QWidget):
|
||||
"""Tab 1: import Excel files and list imported tasks."""
|
||||
|
||||
@@ -56,6 +85,15 @@ class CollectTab(QWidget):
|
||||
self.write_back_worker = None
|
||||
self.write_back_thread = None
|
||||
self.last_collect_run_id = None
|
||||
self._collect_run_started_at = None
|
||||
self._collect_task_started_at = None
|
||||
self._collect_task_elapsed_seconds = 0
|
||||
self._collect_activity_payload = {}
|
||||
self._collect_terminal_text = ""
|
||||
self._collect_stop_requested = False
|
||||
self._collect_elapsed_timer = QTimer(self)
|
||||
self._collect_elapsed_timer.setInterval(1000)
|
||||
self._collect_elapsed_timer.timeout.connect(self._refresh_collect_activity)
|
||||
|
||||
self.import_button = QPushButton("导入 Excel...")
|
||||
self.refresh_button = QPushButton("刷新")
|
||||
@@ -101,6 +139,14 @@ class CollectTab(QWidget):
|
||||
self.summary_label = QLabel("未导入任务")
|
||||
self.summary_label.setTextFormat(Qt.RichText)
|
||||
self.batch_progress_label = _build_batch_progress_overview("collectBatchProgressOverview")
|
||||
self.collect_activity_label = QLabel("")
|
||||
self.collect_activity_label.setObjectName("collectActivityLabel")
|
||||
self.collect_activity_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||
activity_sample = "正在采集 999/999 · 等待商品页加载 · 本条 99:59"
|
||||
activity_width = self.collect_activity_label.fontMetrics().horizontalAdvance(activity_sample) + 24
|
||||
self.collect_activity_label.setFixedWidth(activity_width)
|
||||
self.collect_activity_label.setVisible(False)
|
||||
self._set_collect_activity_style("muted")
|
||||
self.match_detail_label = QLabel("")
|
||||
self.show_all_button = QPushButton("全部")
|
||||
self.show_unmatched_button = QPushButton("未匹配(0)")
|
||||
@@ -112,6 +158,12 @@ class CollectTab(QWidget):
|
||||
summary_layout.addWidget(self.show_all_button)
|
||||
summary_layout.addWidget(self.show_unmatched_button)
|
||||
|
||||
batch_progress_layout = QHBoxLayout()
|
||||
batch_progress_layout.setContentsMargins(0, 0, 0, 0)
|
||||
batch_progress_layout.setSpacing(8)
|
||||
batch_progress_layout.addWidget(self.batch_progress_label, 1)
|
||||
batch_progress_layout.addWidget(self.collect_activity_label)
|
||||
|
||||
self.table = QTableView()
|
||||
self.model = TaskTableModel(self.table)
|
||||
self.table.setModel(self.model)
|
||||
@@ -142,7 +194,7 @@ class CollectTab(QWidget):
|
||||
layout.addLayout(filter_layout)
|
||||
layout.addLayout(summary_layout)
|
||||
layout.addWidget(self.match_detail_label)
|
||||
layout.addWidget(self.batch_progress_label)
|
||||
layout.addLayout(batch_progress_layout)
|
||||
layout.addWidget(self.empty_state_card)
|
||||
layout.addWidget(self.table, 1)
|
||||
layout.addWidget(QLabel("采集运行日志"))
|
||||
@@ -168,6 +220,156 @@ class CollectTab(QWidget):
|
||||
def _set_status(self, message, level=None):
|
||||
_emit_status(self.status_callback, message, level=level)
|
||||
|
||||
def _set_collect_activity_style(self, level):
|
||||
color = _status_level_color(level)
|
||||
self.collect_activity_label.setStyleSheet(
|
||||
"QLabel#collectActivityLabel { "
|
||||
"background: #f6f8fa; border: 1px solid #d0d7de; "
|
||||
"border-radius: 6px; padding: 8px 10px; "
|
||||
f"color: {color}; font-weight: 600; "
|
||||
"}"
|
||||
)
|
||||
|
||||
def _start_collect_activity(self):
|
||||
now = time.monotonic()
|
||||
self._collect_run_started_at = now
|
||||
self._collect_task_started_at = None
|
||||
self._collect_task_elapsed_seconds = 0
|
||||
self._collect_activity_payload = {
|
||||
"state": "preflight_started",
|
||||
"total": 0,
|
||||
"step": "preflight",
|
||||
}
|
||||
self._collect_terminal_text = ""
|
||||
self._collect_stop_requested = False
|
||||
self._refresh_collect_activity(now=now)
|
||||
if not self._collect_elapsed_timer.isActive():
|
||||
self._collect_elapsed_timer.start()
|
||||
|
||||
def _on_collect_activity(self, payload):
|
||||
event = dict(payload or {})
|
||||
state = str(event.get("state") or "")
|
||||
now = time.monotonic()
|
||||
if self._collect_run_started_at is None:
|
||||
self._collect_run_started_at = now
|
||||
self._collect_terminal_text = ""
|
||||
if not self._collect_elapsed_timer.isActive():
|
||||
self._collect_elapsed_timer.start()
|
||||
|
||||
if state == "preflight_started":
|
||||
self._collect_activity_payload = event
|
||||
elif state == "task_started":
|
||||
self._collect_task_started_at = now
|
||||
self._collect_task_elapsed_seconds = 0
|
||||
self._collect_activity_payload = event
|
||||
elif state in {"task_step", "task_finished"}:
|
||||
current = dict(self._collect_activity_payload)
|
||||
current.update(event)
|
||||
self._collect_activity_payload = current
|
||||
if state == "task_finished" and self._collect_task_started_at is not None:
|
||||
self._collect_task_elapsed_seconds = max(
|
||||
0,
|
||||
int(now - self._collect_task_started_at),
|
||||
)
|
||||
self._collect_task_started_at = None
|
||||
self._refresh_collect_activity(now=now)
|
||||
|
||||
def _refresh_collect_activity(self, now=None):
|
||||
if self._collect_terminal_text:
|
||||
self.collect_activity_label.setVisible(True)
|
||||
self.collect_activity_label.setText(self._collect_terminal_text)
|
||||
return
|
||||
if self._collect_run_started_at is None:
|
||||
self.collect_activity_label.setVisible(False)
|
||||
return
|
||||
|
||||
current_time = time.monotonic() if now is None else now
|
||||
run_elapsed = max(0, int(current_time - self._collect_run_started_at))
|
||||
if self._collect_task_started_at is not None:
|
||||
task_elapsed = max(0, int(current_time - self._collect_task_started_at))
|
||||
else:
|
||||
task_elapsed = self._collect_task_elapsed_seconds
|
||||
event = self._collect_activity_payload
|
||||
state = str(event.get("state") or "")
|
||||
index = int(event.get("index") or 0)
|
||||
total = int(event.get("total") or 0)
|
||||
progress = f"{index}/{total}" if index and total else str(index or total or "")
|
||||
step_label = _collect_activity_step_label(event.get("step"))
|
||||
|
||||
if self._collect_stop_requested:
|
||||
if index:
|
||||
text = f"正在停止 · 本条 {_format_collect_elapsed(task_elapsed)}"
|
||||
else:
|
||||
text = f"正在停止 · 已等待 {_format_collect_elapsed(run_elapsed)}"
|
||||
level = "warning"
|
||||
elif state in {"task_started", "task_step"}:
|
||||
text = (
|
||||
f"正在采集 {progress} · {step_label} · "
|
||||
f"本条 {_format_collect_elapsed(task_elapsed)}"
|
||||
)
|
||||
level = "info"
|
||||
elif state == "task_finished":
|
||||
result_label = {
|
||||
"success": "已完成",
|
||||
"failed": "失败",
|
||||
"skipped": "略过",
|
||||
}.get(str(event.get("result") or ""), "已结束")
|
||||
text = (
|
||||
f"第 {progress} 条 · {result_label} · "
|
||||
f"本条 {_format_collect_elapsed(task_elapsed)}"
|
||||
)
|
||||
level = "danger" if event.get("result") == "failed" else "muted"
|
||||
else:
|
||||
text = f"正在检查账号 · {_format_collect_elapsed(run_elapsed)}"
|
||||
level = "info"
|
||||
|
||||
tooltip_parts = []
|
||||
if event.get("item_id"):
|
||||
tooltip_parts.append(f"商品ID:{event.get('item_id')}")
|
||||
if event.get("alias"):
|
||||
tooltip_parts.append(f"账号别名:{event.get('alias')}")
|
||||
if state in {"task_started", "task_step"}:
|
||||
tooltip_parts.append(f"当前阶段:{step_label}")
|
||||
self.collect_activity_label.setToolTip("\n".join(tooltip_parts))
|
||||
self._set_collect_activity_style(level)
|
||||
self.collect_activity_label.setText(text)
|
||||
self.collect_activity_label.setVisible(True)
|
||||
|
||||
def _finish_collect_activity(self, outcome, payload=None):
|
||||
now = time.monotonic()
|
||||
if self._collect_run_started_at is None:
|
||||
total_elapsed = 0
|
||||
else:
|
||||
total_elapsed = max(0, int(now - self._collect_run_started_at))
|
||||
self._collect_elapsed_timer.stop()
|
||||
self._collect_run_started_at = None
|
||||
self._collect_task_started_at = None
|
||||
self._collect_task_elapsed_seconds = 0
|
||||
self._collect_activity_payload = {}
|
||||
self._collect_stop_requested = False
|
||||
|
||||
if outcome == "blocked":
|
||||
text = "采集未开始 · 检查未通过"
|
||||
level = "warning"
|
||||
tooltip = "采集前检查未通过,请按弹窗提示处理"
|
||||
elif outcome == "cancelled":
|
||||
text = f"采集已停止 · 总用时 {_format_collect_elapsed(total_elapsed)}"
|
||||
level = "warning"
|
||||
tooltip = "本轮采集已停止"
|
||||
elif outcome == "error":
|
||||
text = "采集已结束 · 请查看运行日志"
|
||||
level = "danger"
|
||||
tooltip = "采集异常结束,请查看下方采集运行日志"
|
||||
else:
|
||||
text = f"采集完成 · 总用时 {_format_collect_elapsed(total_elapsed)}"
|
||||
level = "success"
|
||||
tooltip = "本轮采集已经完成"
|
||||
self._collect_terminal_text = text
|
||||
self.collect_activity_label.setToolTip(tooltip)
|
||||
self._set_collect_activity_style(level)
|
||||
self.collect_activity_label.setText(text)
|
||||
self.collect_activity_label.setVisible(True)
|
||||
|
||||
def _on_collect_log(self, message):
|
||||
self._append_collect_log(message)
|
||||
self._set_status(message)
|
||||
@@ -522,6 +724,9 @@ class CollectTab(QWidget):
|
||||
config=self.config,
|
||||
diagnostic_log_dir=diagnostics.DEFAULT_LOG_DIR,
|
||||
)
|
||||
activity_signal = getattr(worker, "activity", None)
|
||||
if activity_signal is not None:
|
||||
activity_signal.connect(self._on_collect_activity)
|
||||
worker.progress.connect(self._on_collect_progress)
|
||||
worker.row_updated.connect(self._on_collect_row_updated)
|
||||
worker.log.connect(self._on_collect_log)
|
||||
@@ -534,11 +739,14 @@ class CollectTab(QWidget):
|
||||
self.collect_worker = worker
|
||||
self.collect_thread = thread
|
||||
self._set_collect_running(True)
|
||||
self._start_collect_activity()
|
||||
thread.start()
|
||||
|
||||
def stop_collect(self, checked=False):
|
||||
if self.collect_worker is not None:
|
||||
self.collect_worker.cancel()
|
||||
self._collect_stop_requested = True
|
||||
self._refresh_collect_activity()
|
||||
self._set_status("正在停止采集...")
|
||||
|
||||
def write_back_old_data(self, checked=False):
|
||||
@@ -616,6 +824,8 @@ class CollectTab(QWidget):
|
||||
|
||||
def _forget_collect_thread(self, thread):
|
||||
if self.collect_thread is thread:
|
||||
if self._collect_elapsed_timer.isActive():
|
||||
self._finish_collect_activity("error")
|
||||
self.collect_thread = None
|
||||
self.collect_worker = None
|
||||
|
||||
@@ -643,6 +853,12 @@ class CollectTab(QWidget):
|
||||
|
||||
def _on_collect_finished(self, payload):
|
||||
self._set_collect_running(False)
|
||||
if payload.get("blocked"):
|
||||
self._finish_collect_activity("blocked", payload)
|
||||
elif payload.get("error"):
|
||||
self._finish_collect_activity("error", payload)
|
||||
else:
|
||||
self._finish_collect_activity("finished", payload)
|
||||
self.last_collect_run_id = payload.get("run_id") or self.last_collect_run_id
|
||||
self.refresh_tasks()
|
||||
self._load_latest_collect_run_log()
|
||||
@@ -747,6 +963,7 @@ class CollectTab(QWidget):
|
||||
|
||||
def _on_collect_cancelled(self, payload):
|
||||
self._set_collect_running(False)
|
||||
self._finish_collect_activity("cancelled", payload)
|
||||
self.refresh_tasks()
|
||||
self._set_status(
|
||||
"采集已停止:完成{done}/{total}".format(
|
||||
|
||||
+115
-1
@@ -7,6 +7,11 @@ import re
|
||||
import threading
|
||||
import time
|
||||
|
||||
try:
|
||||
from PySide6.QtCore import Signal
|
||||
except ModuleNotFoundError: # pragma: no cover - GUI import guard
|
||||
Signal = None
|
||||
|
||||
from .. import ai, image_studio, image_studio_export, image_studio_generation, image_studio_images
|
||||
from ..collect_skip import ALIAS_UNMATCHED, LOGIN_REQUIRED, empty_skip_reason_counts
|
||||
from .widgets import *
|
||||
@@ -1743,6 +1748,9 @@ class ApplyWorker(BaseWorker):
|
||||
class CollectWorker(BaseWorker):
|
||||
"""Collect old title and cover for imported tasks."""
|
||||
|
||||
if Signal is not None:
|
||||
activity = Signal(dict)
|
||||
|
||||
LOGIN_CHECK_ATTEMPTS = 3
|
||||
LOGIN_CHECK_RETRY_DELAY_SECONDS = 2.0
|
||||
|
||||
@@ -1785,6 +1793,11 @@ class CollectWorker(BaseWorker):
|
||||
skip_reason_counts = empty_skip_reason_counts()
|
||||
|
||||
self._run_id = self._create_run_log(eligible, batch_ids)
|
||||
self._emit_activity(
|
||||
"preflight_started",
|
||||
total=total,
|
||||
step="preflight",
|
||||
)
|
||||
self._log_run_event(
|
||||
f"step=preflight result=start detail=采集运行开始 total={total}"
|
||||
)
|
||||
@@ -1822,9 +1835,16 @@ class CollectWorker(BaseWorker):
|
||||
level="warning",
|
||||
)
|
||||
|
||||
for task in eligible:
|
||||
for index, task in enumerate(eligible, start=1):
|
||||
if self.should_cancel():
|
||||
break
|
||||
self._emit_activity(
|
||||
"task_started",
|
||||
task=task,
|
||||
index=index,
|
||||
total=total,
|
||||
step="match_account",
|
||||
)
|
||||
account = account_by_alias.get(str(task.alias).strip())
|
||||
if account is None:
|
||||
skipped += 1
|
||||
@@ -1842,6 +1862,14 @@ class CollectWorker(BaseWorker):
|
||||
task=task,
|
||||
level="warning",
|
||||
)
|
||||
self._emit_activity(
|
||||
"task_finished",
|
||||
task=task,
|
||||
index=index,
|
||||
total=total,
|
||||
step="match_account",
|
||||
result="skipped",
|
||||
)
|
||||
self._emit_progress(done, total, collected, skipped, failed)
|
||||
continue
|
||||
|
||||
@@ -1862,9 +1890,24 @@ class CollectWorker(BaseWorker):
|
||||
task=task,
|
||||
level="warning",
|
||||
)
|
||||
self._emit_activity(
|
||||
"task_finished",
|
||||
task=task,
|
||||
index=index,
|
||||
total=total,
|
||||
step="check_login",
|
||||
result="skipped",
|
||||
)
|
||||
self._emit_progress(done, total, collected, skipped, failed)
|
||||
continue
|
||||
|
||||
self._emit_activity(
|
||||
"task_step",
|
||||
task=task,
|
||||
index=index,
|
||||
total=total,
|
||||
step="check_login",
|
||||
)
|
||||
status = self._confirmed_login_status(account, context="midrun", task=task)
|
||||
if self._is_definitive_logged_out(status):
|
||||
alias = str(task.alias).strip()
|
||||
@@ -1885,6 +1928,14 @@ class CollectWorker(BaseWorker):
|
||||
task=task,
|
||||
level="warning",
|
||||
)
|
||||
self._emit_activity(
|
||||
"task_finished",
|
||||
task=task,
|
||||
index=index,
|
||||
total=total,
|
||||
step="check_login",
|
||||
result="skipped",
|
||||
)
|
||||
self._emit_progress(done, total, collected, skipped, failed)
|
||||
continue
|
||||
if not status.get("logged_in"):
|
||||
@@ -1900,10 +1951,18 @@ class CollectWorker(BaseWorker):
|
||||
|
||||
started = time.monotonic()
|
||||
current_step = "db_write"
|
||||
activity_result = "success"
|
||||
|
||||
def on_step(step):
|
||||
nonlocal current_step
|
||||
current_step = str(step)
|
||||
self._emit_activity(
|
||||
"task_step",
|
||||
task=task,
|
||||
index=index,
|
||||
total=total,
|
||||
step=current_step,
|
||||
)
|
||||
self._log_run_event(
|
||||
"step={step} result=start detail=任务 {task_id} 商品 {item_id}".format(
|
||||
step=current_step,
|
||||
@@ -1914,6 +1973,13 @@ class CollectWorker(BaseWorker):
|
||||
)
|
||||
|
||||
try:
|
||||
self._emit_activity(
|
||||
"task_step",
|
||||
task=task,
|
||||
index=index,
|
||||
total=total,
|
||||
step="prepare_task",
|
||||
)
|
||||
self._log_run_event(
|
||||
"step=db_write result=start detail=任务 {task_id} 商品 {item_id} 标记采集运行".format(
|
||||
task_id=task.id,
|
||||
@@ -1932,6 +1998,13 @@ class CollectWorker(BaseWorker):
|
||||
on_step=on_step,
|
||||
)
|
||||
current_step = "db_write"
|
||||
self._emit_activity(
|
||||
"task_step",
|
||||
task=task,
|
||||
index=index,
|
||||
total=total,
|
||||
step="save_result",
|
||||
)
|
||||
self._log_run_event(
|
||||
"step=db_write result=start detail=任务 {task_id} 商品 {item_id} 保存采集结果".format(
|
||||
task_id=task.id,
|
||||
@@ -1965,6 +2038,7 @@ class CollectWorker(BaseWorker):
|
||||
task=task,
|
||||
)
|
||||
except Exception as exc:
|
||||
activity_result = "failed"
|
||||
failed += 1
|
||||
error = str(exc) or exc.__class__.__name__
|
||||
safe_error = diagnostics.redact_log_text(error)
|
||||
@@ -1993,6 +2067,14 @@ class CollectWorker(BaseWorker):
|
||||
)
|
||||
finally:
|
||||
done += 1
|
||||
self._emit_activity(
|
||||
"task_finished",
|
||||
task=task,
|
||||
index=index,
|
||||
total=total,
|
||||
step=current_step,
|
||||
result=activity_result,
|
||||
)
|
||||
self._emit_progress(done, total, collected, skipped, failed)
|
||||
|
||||
summary = self._summary(
|
||||
@@ -2115,6 +2197,38 @@ class CollectWorker(BaseWorker):
|
||||
payload["reason"] = reason
|
||||
return payload
|
||||
|
||||
def _emit_activity(
|
||||
self,
|
||||
state,
|
||||
*,
|
||||
task=None,
|
||||
index=0,
|
||||
total=0,
|
||||
step=None,
|
||||
result=None,
|
||||
):
|
||||
signal = getattr(self, "activity", None)
|
||||
if signal is None:
|
||||
return
|
||||
payload = {
|
||||
"state": str(state),
|
||||
"index": int(index or 0),
|
||||
"total": int(total or 0),
|
||||
}
|
||||
if task is not None:
|
||||
payload.update(
|
||||
{
|
||||
"task_id": getattr(task, "id", None),
|
||||
"item_id": str(getattr(task, "item_id", "") or ""),
|
||||
"alias": str(getattr(task, "alias", "") or ""),
|
||||
}
|
||||
)
|
||||
if step:
|
||||
payload["step"] = str(step)
|
||||
if result:
|
||||
payload["result"] = str(result)
|
||||
signal.emit(payload)
|
||||
|
||||
def _emit_progress(self, done, total, collected, skipped, failed):
|
||||
self.progress.emit(
|
||||
{
|
||||
|
||||
+8
-1
@@ -3,7 +3,7 @@ id: T-628
|
||||
title: 采集当前记录阶段与单条耗时指示器
|
||||
phase: 2
|
||||
deps: [T-620]
|
||||
status: TODO
|
||||
status: DONE
|
||||
created: 2026-07-14
|
||||
---
|
||||
|
||||
@@ -86,3 +86,10 @@ created: 2026-07-14
|
||||
- 不修改任务 `stage/status`、SQLite schema、Excel schema、回写流程或批次统计口径。
|
||||
- 不把计时器放进 Worker 线程,不让 GUI 每秒查询 SQLite,也不把每秒 tick 写入运行日志。
|
||||
- 不改变②AI生成、③更新蝦皮及⑥商品套图的计时显示。
|
||||
|
||||
## 执行记录
|
||||
|
||||
- 2026-07-14:`CollectWorker` 新增独立 `activity` 结构化信号,在前置检查、每条记录开始、采集步骤变化及单条结束时发送稳定 payload;每条记录进入循环即发 `task_started`,登录检查和 `editor.collect()` 步骤只更新阶段,不改变原有进度、日志、状态写库和终态信号。
|
||||
- 2026-07-14:①「导入采集」在批次进度同行右侧新增固定宽度运行指示器,显示前置检查、`N/总数`、当前中文阶段和本条 `MM:SS`;基于 `time.monotonic()` 每秒刷新,每条开始重置,步骤变化不重置,停止请求期间继续显示实际等待,完成、阻断、停止和异常后冻结中文终态。
|
||||
- 2026-07-14:新增 Worker 活动事件顺序和 GUI 计时生命周期测试,覆盖三条记录的成功/略过事件、步骤不重置、下一条归零、停止等待、终态冻结、小时格式及中文 tooltip;用户可见终态不展示底层英文异常。
|
||||
- 验证:当前工作区全套 GUI 测试除任务开始前已存在的默认封面提示词 `papa1.txt` 改名造成的 2 项旧断言外,采集相关测试全部通过;在仅覆盖 T-628 四个文件的隔离 worktree 中运行 `python -m ruff check app tests main.py`、`py -3.10 -m compileall app main.py`、`py -3.10 -m unittest discover -s tests`(461 项通过)及 `git diff --check`,结果全部通过。
|
||||
|
||||
+153
-6
@@ -65,6 +65,7 @@ from app.gui import (
|
||||
)
|
||||
from app.gui import file_manager
|
||||
import app.gui.workers as gui_workers
|
||||
from app.gui.tabs.collect import _format_collect_elapsed
|
||||
from app.gui.tabs.generate import CoverGalleryDialog, OriginalImageDialog
|
||||
from app.gui.main_window import _fit_and_center_window
|
||||
|
||||
@@ -7391,6 +7392,130 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_collect_activity_tracks_step_resets_each_task_and_freezes_on_stop(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
tab = CollectTab(config=self.make_config(temp_dir))
|
||||
self.addCleanup(tab.close)
|
||||
|
||||
self.assertEqual("collectActivityLabel", tab.collect_activity_label.objectName())
|
||||
self.assertTrue(tab.collect_activity_label.isHidden())
|
||||
self.assertEqual("1:01:01", _format_collect_elapsed(3661))
|
||||
|
||||
with mock.patch("app.gui.tabs.collect.time.monotonic", return_value=100.0):
|
||||
tab._start_collect_activity()
|
||||
self.assertTrue(tab._collect_elapsed_timer.isActive())
|
||||
self.assertIn("正在检查账号", tab.collect_activity_label.text())
|
||||
self.assertIn("00:00", tab.collect_activity_label.text())
|
||||
|
||||
with mock.patch("app.gui.tabs.collect.time.monotonic", return_value=105.0):
|
||||
tab._on_collect_activity(
|
||||
{
|
||||
"state": "preflight_started",
|
||||
"total": 2,
|
||||
"step": "preflight",
|
||||
}
|
||||
)
|
||||
self.assertIn("正在检查账号", tab.collect_activity_label.text())
|
||||
self.assertIn("00:05", tab.collect_activity_label.text())
|
||||
|
||||
with mock.patch("app.gui.tabs.collect.time.monotonic", return_value=110.0):
|
||||
tab._on_collect_activity(
|
||||
{
|
||||
"state": "task_started",
|
||||
"index": 1,
|
||||
"total": 2,
|
||||
"task_id": 1,
|
||||
"item_id": "51100639510",
|
||||
"alias": "alias-a",
|
||||
"step": "match_account",
|
||||
}
|
||||
)
|
||||
self.assertEqual(
|
||||
"正在采集 1/2 · 匹配账号 · 本条 00:00",
|
||||
tab.collect_activity_label.text(),
|
||||
)
|
||||
|
||||
with mock.patch("app.gui.tabs.collect.time.monotonic", return_value=117.0):
|
||||
tab._on_collect_activity(
|
||||
{
|
||||
"state": "task_step",
|
||||
"index": 1,
|
||||
"total": 2,
|
||||
"task_id": 1,
|
||||
"item_id": "51100639510",
|
||||
"alias": "alias-a",
|
||||
"step": "wait_ready",
|
||||
}
|
||||
)
|
||||
self.assertEqual(
|
||||
"正在采集 1/2 · 等待商品页加载 · 本条 00:07",
|
||||
tab.collect_activity_label.text(),
|
||||
)
|
||||
self.assertIn("商品ID:51100639510", tab.collect_activity_label.toolTip())
|
||||
self.assertIn("当前阶段:等待商品页加载", tab.collect_activity_label.toolTip())
|
||||
|
||||
with mock.patch("app.gui.tabs.collect.time.monotonic", return_value=120.0):
|
||||
tab._on_collect_activity(
|
||||
{
|
||||
"state": "task_started",
|
||||
"index": 2,
|
||||
"total": 2,
|
||||
"task_id": 2,
|
||||
"item_id": "51100639511",
|
||||
"alias": "alias-a",
|
||||
"step": "check_login",
|
||||
}
|
||||
)
|
||||
self.assertEqual(
|
||||
"正在采集 2/2 · 检查账号登录 · 本条 00:00",
|
||||
tab.collect_activity_label.text(),
|
||||
)
|
||||
|
||||
with mock.patch("app.gui.tabs.collect.time.monotonic", return_value=125.0):
|
||||
tab._collect_stop_requested = True
|
||||
tab._refresh_collect_activity()
|
||||
self.assertEqual("正在停止 · 本条 00:05", tab.collect_activity_label.text())
|
||||
|
||||
with mock.patch("app.gui.tabs.collect.time.monotonic", return_value=130.0):
|
||||
tab._finish_collect_activity("cancelled")
|
||||
self.assertEqual("采集已停止 · 总用时 00:30", tab.collect_activity_label.text())
|
||||
self.assertFalse(tab._collect_elapsed_timer.isActive())
|
||||
|
||||
frozen_text = tab.collect_activity_label.text()
|
||||
with mock.patch("app.gui.tabs.collect.time.monotonic", return_value=999.0):
|
||||
tab._refresh_collect_activity()
|
||||
self.assertEqual(frozen_text, tab.collect_activity_label.text())
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_collect_activity_terminal_states_use_chinese_semantics(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
tab = CollectTab(config=self.make_config(temp_dir))
|
||||
self.addCleanup(tab.close)
|
||||
|
||||
with mock.patch("app.gui.tabs.collect.time.monotonic", return_value=10.0):
|
||||
tab._start_collect_activity()
|
||||
with mock.patch("app.gui.tabs.collect.time.monotonic", return_value=15.0):
|
||||
tab._finish_collect_activity("blocked")
|
||||
self.assertEqual("采集未开始 · 检查未通过", tab.collect_activity_label.text())
|
||||
self.assertFalse(tab._collect_elapsed_timer.isActive())
|
||||
|
||||
with mock.patch("app.gui.tabs.collect.time.monotonic", return_value=20.0):
|
||||
tab._start_collect_activity()
|
||||
with mock.patch("app.gui.tabs.collect.time.monotonic", return_value=25.0):
|
||||
tab._finish_collect_activity("error", {"error": "后台线程异常"})
|
||||
self.assertEqual("采集已结束 · 请查看运行日志", tab.collect_activity_label.text())
|
||||
self.assertEqual("采集异常结束,请查看下方采集运行日志", tab.collect_activity_label.toolTip())
|
||||
|
||||
with mock.patch("app.gui.tabs.collect.time.monotonic", return_value=30.0):
|
||||
tab._start_collect_activity()
|
||||
with mock.patch("app.gui.tabs.collect.time.monotonic", return_value=40.0):
|
||||
tab._finish_collect_activity("finished")
|
||||
self.assertEqual("采集完成 · 总用时 00:10", tab.collect_activity_label.text())
|
||||
self.assertFalse(tab._collect_elapsed_timer.isActive())
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_collect_tab_soft_deletes_batch_and_refreshes_workflow_tabs(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
@@ -7706,14 +7831,17 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
"old_cover_path": task["old_cover_path"],
|
||||
}
|
||||
|
||||
worker = CollectWorker(
|
||||
tasks,
|
||||
db_path=cfg["db_path"],
|
||||
config=cfg,
|
||||
preflight=False,
|
||||
)
|
||||
activity_events = []
|
||||
worker.activity.connect(activity_events.append)
|
||||
with mock.patch("app.gui.accounts.detect_login", side_effect=fake_login), \
|
||||
mock.patch("app.gui.editor.collect", side_effect=fake_collect) as collect:
|
||||
summary = CollectWorker(
|
||||
tasks,
|
||||
db_path=cfg["db_path"],
|
||||
config=cfg,
|
||||
preflight=False,
|
||||
).execute()
|
||||
summary = worker.execute()
|
||||
|
||||
self.assertTrue(summary["ok"])
|
||||
self.assertEqual(3, summary["total"])
|
||||
@@ -7728,6 +7856,25 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
self.assertEqual([batch_id], summary["batch_ids"])
|
||||
self.assertIsInstance(summary["run_id"], int)
|
||||
collect.assert_called_once()
|
||||
self.assertEqual("preflight_started", activity_events[0]["state"])
|
||||
self.assertEqual(3, activity_events[0]["total"])
|
||||
task_starts = [event for event in activity_events if event["state"] == "task_started"]
|
||||
self.assertEqual([1, 2, 3], [event["index"] for event in task_starts])
|
||||
self.assertEqual(
|
||||
["51100639510", "51100639511", "51100639512"],
|
||||
[event["item_id"] for event in task_starts],
|
||||
)
|
||||
self.assertTrue(
|
||||
any(
|
||||
event["state"] == "task_step" and event.get("step") == "download_cover"
|
||||
for event in activity_events
|
||||
)
|
||||
)
|
||||
task_finishes = [event for event in activity_events if event["state"] == "task_finished"]
|
||||
self.assertEqual(
|
||||
["success", "skipped", "skipped"],
|
||||
[event["result"] for event in task_finishes],
|
||||
)
|
||||
updated = db.list_tasks(batch_id=batch_id, path=cfg["db_path"])
|
||||
by_alias = {task.alias: task for task in updated}
|
||||
self.assertEqual("collected", by_alias["alias-a"].stage)
|
||||
|
||||
Reference in New Issue
Block a user