feat(collect): show per-item collection activity timer

This commit is contained in:
chengma
2026-07-14 11:10:36 +08:00
parent da1da13fdf
commit 0abd7ffdff
4 changed files with 494 additions and 9 deletions
+218 -1
View File
@@ -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
View File
@@ -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(
{