feat(collect): choose product status scope

This commit is contained in:
chengma
2026-07-18 16:46:59 +08:00
parent 0cf23db403
commit 4658a1ca61
11 changed files with 420 additions and 41 deletions
+35 -20
View File
@@ -1166,26 +1166,41 @@ def collect(account, task, on_step=None) -> dict:
try:
_notify_collect_step(on_step, "read_product_status")
status_snapshot = read_product_status(cdp)
_notify_collect_step(on_step, "read_title")
old_title = read_title(cdp)
_notify_collect_step(on_step, "read_cover")
old_cover_src = read_cover_src(cdp)
out_path = _get(task, "old_cover_path")
if not out_path:
out_path = image_paths.task_image_path(
appconfig.image_dir(),
task,
account,
"old",
)
_notify_collect_step(on_step, "download_cover")
old_cover_path = download_cover(old_cover_src, out_path)
result = {
"old_title": old_title,
"old_cover_src": old_cover_src,
"old_cover_path": old_cover_path,
**status_snapshot,
}
collect_scope = product_status.normalize_collect_scope(
_get(task, "collection_scope")
)
if not product_status.should_collect_content(
status_snapshot.get("product_status"),
collect_scope,
):
result = {
**status_snapshot,
"collection_skipped": True,
"collection_skip_reason": product_status.collect_skip_reason(
status_snapshot.get("product_status")
),
}
else:
_notify_collect_step(on_step, "read_title")
old_title = read_title(cdp)
_notify_collect_step(on_step, "read_cover")
old_cover_src = read_cover_src(cdp)
out_path = _get(task, "old_cover_path")
if not out_path:
out_path = image_paths.task_image_path(
appconfig.image_dir(),
task,
account,
"old",
)
_notify_collect_step(on_step, "download_cover")
old_cover_path = download_cover(old_cover_src, out_path)
result = {
"old_title": old_title,
"old_cover_src": old_cover_src,
"old_cover_path": old_cover_path,
**status_snapshot,
}
finally:
close_target_confirmed = _close_collected_product(cdp)
result["close_target_confirmed"] = close_target_confirmed
+60 -3
View File
@@ -8,6 +8,7 @@ from ...collect_skip import (
format_skip_reason_summary,
normalize_skip_reason_counts,
)
from ... import product_status
from ..models import TaskTableModel
from ..widgets import *
from ..workers import CollectWorker as _RealCollectWorker, WriteBackWorker as _RealWriteBackWorker
@@ -28,6 +29,7 @@ COLLECT_ACTIVITY_STEP_LABELS = {
"prepare_task": "准备采集",
"open_product": "打开商品页",
"wait_ready": "等待商品页加载",
"read_product_status": "读取商品状态",
"read_title": "读取标题",
"read_cover": "读取封面",
"download_cover": "下载封面",
@@ -718,11 +720,16 @@ class CollectTab(QWidget):
if not tasks:
self._set_status("没有可采集任务")
return
collect_scope = self._choose_collect_scope()
if collect_scope is None:
self._set_status("已取消采集")
return
worker = CollectWorker(
tasks,
db_path=self.db_path,
config=self.config,
diagnostic_log_dir=diagnostics.DEFAULT_LOG_DIR,
collect_scope=collect_scope,
)
activity_signal = getattr(worker, "activity", None)
if activity_signal is not None:
@@ -742,6 +749,32 @@ class CollectTab(QWidget):
self._start_collect_activity()
thread.start()
def _choose_collect_scope(self):
box = QMessageBox(self)
box.setIcon(QMessageBox.Question)
box.setWindowTitle("选择采集范围")
box.setText("请选择本轮要采集的商品范围。")
box.setInformativeText(
"程序会逐个打开商品详情页检测状态并保存结果。"
"采集所有状态商品包含未上架、审核中和状态未知商品,"
"可能增加采集时间,但本步骤不消耗 AI 点数。"
)
normal_button = box.addButton("只采集状态正常的商品", QMessageBox.AcceptRole)
normal_button.setObjectName("collectNormalOnlyButton")
all_button = box.addButton("采集所有状态的商品", QMessageBox.DestructiveRole)
all_button.setObjectName("collectAllStatusesButton")
all_button.setStyleSheet("color: #cf222e; font-weight: 600;")
cancel_button = box.addButton("取消", QMessageBox.RejectRole)
cancel_button.setObjectName("collectScopeCancelButton")
box.setDefaultButton(normal_button)
box.setEscapeButton(cancel_button)
box.exec()
if box.clickedButton() is normal_button:
return product_status.COLLECT_SCOPE_NORMAL_ONLY
if box.clickedButton() is all_button:
return product_status.COLLECT_SCOPE_ALL
return None
def stop_collect(self, checked=False):
if self.collect_worker is not None:
self.collect_worker.cancel()
@@ -870,6 +903,17 @@ class CollectTab(QWidget):
skipped=payload.get("skipped", 0),
failed=payload.get("failed", 0),
)
status_counts = payload.get("product_status_counts") or {}
status_skipped = int(payload.get("status_scope_skipped", 0) or 0)
if status_counts:
message += ";状态正常{normal},未上架{unlisted},审核中{reviewing},状态未知{unknown}".format(
normal=status_counts.get("normal", 0),
unlisted=status_counts.get("unlisted", 0),
reviewing=status_counts.get("reviewing", 0),
unknown=status_counts.get("unknown", 0),
)
if status_skipped:
message += f";按范围略过{status_skipped}"
self._show_collect_account_summary(payload, message)
if payload.get("collected", 0) > 0:
batch_id = self._active_batch_id()
@@ -917,14 +961,25 @@ class CollectTab(QWidget):
reused = payload.get("reused_accounts") or []
login_required = payload.get("login_required_accounts") or []
skipped = max(0, int(payload.get("skipped", 0) or 0))
status_scope_skipped = int(payload.get("status_scope_skipped", 0) or 0)
account_skipped = max(0, skipped - status_scope_skipped)
skip_counts = normalize_skip_reason_counts(
payload.get("skip_reason_counts"),
skipped_total=skipped,
skipped_total=account_skipped,
)
if not launched and not reused and not login_required and skipped == 0:
if (
not launched
and not reused
and not login_required
and account_skipped == 0
and status_scope_skipped == 0
):
return
lines = [message]
skip_summary = format_skip_reason_summary(skip_counts, skipped_total=skipped)
skip_summary = format_skip_reason_summary(
skip_counts,
skipped_total=account_skipped,
)
if skip_summary:
lines.append(skip_summary)
if skip_counts[ALIAS_UNMATCHED] > 0:
@@ -941,6 +996,8 @@ class CollectTab(QWidget):
)
if skip_counts[LOGIN_REQUIRED] > 0:
lines.append("请到账号管理完成对应账号登录后,再重新采集略过任务。")
if status_scope_skipped:
lines.append(f"本轮按范围略过{status_scope_skipped}个非正常状态商品,未下载标题和封面。")
if launched or reused or login_required:
lines.append("采集结束后不会自动关闭账号 Chrome,请按需自行关闭。")
text = "\n".join(lines)
+56 -8
View File
@@ -20,6 +20,7 @@ from .. import (
image_studio_export,
image_studio_generation,
image_studio_images,
product_status,
)
from ..collect_skip import ALIAS_UNMATCHED, LOGIN_REQUIRED, empty_skip_reason_counts
from .widgets import *
@@ -1878,6 +1879,7 @@ class CollectWorker(BaseWorker):
config=None,
preflight=True,
diagnostic_log_dir=None,
collect_scope="all",
):
super().__init__()
self.tasks = list(tasks)
@@ -1885,6 +1887,7 @@ class CollectWorker(BaseWorker):
self.config = config
self.preflight = preflight
self.diagnostic_log_dir = diagnostic_log_dir
self.collect_scope = product_status.normalize_collect_scope(collect_scope)
self._run_id = None
def execute(self):
@@ -1908,6 +1911,10 @@ class CollectWorker(BaseWorker):
login_required_accounts = {}
preflight_info = {}
skip_reason_counts = empty_skip_reason_counts()
product_status_counts = {
status: 0 for status in product_status.VALID_PRODUCT_STATUSES
}
status_scope_skipped = 0
self._run_id = self._create_run_log(eligible, batch_ids)
self._emit_activity(
@@ -1935,6 +1942,9 @@ class CollectWorker(BaseWorker):
extra={
**blocked,
"skip_reason_counts": dict(skip_reason_counts),
"collect_scope": self.collect_scope,
"product_status_counts": dict(product_status_counts),
"status_scope_skipped": status_scope_skipped,
},
)
self._finish_run_log("blocked", summary)
@@ -2111,9 +2121,22 @@ class CollectWorker(BaseWorker):
{
"item_id": task.item_id,
"old_cover_path": self._old_cover_path(account, task),
"collection_scope": self.collect_scope,
},
on_step=on_step,
)
detected_status = product_status.normalize_status(
result.get("product_status")
)
product_status_counts[detected_status] += 1
if result.get("product_status_error"):
self._write_diagnostic_log(
"商品状态检测失败,已按状态未知保存",
level="WARNING",
step="read_product_status",
task=task,
payload={"error": result.get("product_status_error")},
)
if result.get("close_target_confirmed") is False:
self._log_run_event(
"step=close_product result=uncertain detail=任务 {task_id} 商品 {item_id} 商品页已请求关闭,但未在短时间内确认关闭;采集结果已保留,继续处理后续任务".format(
@@ -2133,6 +2156,35 @@ class CollectWorker(BaseWorker):
"close_target_confirmed": False,
},
)
if result.get("collection_skipped"):
activity_result = "skipped"
current_step = "read_product_status"
reason = result.get("collection_skip_reason") or product_status.collect_skip_reason(
detected_status
)
db.set_product_status(
task.id,
detected_status,
result.get("product_status_note"),
path=self.db_path,
)
db.mark_skipped(task.id, reason, path=self.db_path)
skipped += 1
status_scope_skipped += 1
self.row_updated.emit(
task.id,
{"status": "skipped", "last_error": reason},
)
self._log_run_event(
"step=read_product_status result=skipped detail=任务 {task_id} 商品 {item_id} {reason}".format(
task_id=task.id,
item_id=task.item_id,
reason=reason,
),
task=task,
level="warning",
)
continue
current_step = "db_write"
self._emit_activity(
"task_step",
@@ -2156,14 +2208,6 @@ class CollectWorker(BaseWorker):
product_status_note=result.get("product_status_note"),
path=self.db_path,
)
if result.get("product_status_error"):
self._write_diagnostic_log(
"商品状态检测失败,已按状态未知保存",
level="WARNING",
step="read_product_status",
task=task,
payload={"error": result.get("product_status_error")},
)
collected += 1
elapsed_ms = self._elapsed_ms(started)
self.row_updated.emit(
@@ -2235,6 +2279,9 @@ class CollectWorker(BaseWorker):
**preflight_info,
"login_required_accounts": list(login_required_accounts.values()),
"skip_reason_counts": dict(skip_reason_counts),
"collect_scope": self.collect_scope,
"product_status_counts": dict(product_status_counts),
"status_scope_skipped": status_scope_skipped,
},
)
self._finish_run_log("cancelled" if self.should_cancel() else "done", summary)
@@ -2540,6 +2587,7 @@ class CollectWorker(BaseWorker):
options={
"batch_ids": batch_ids,
"preflight": self.preflight,
"collect_scope": self.collect_scope,
},
path=self.db_path,
)
+21
View File
@@ -26,6 +26,14 @@ PRODUCT_STATUS_LABELS = {
STATUS_UNKNOWN: "状态未知",
}
SCOPE_NORMAL_ONLY = "normal_only"
SCOPE_ALL = "all"
# Kept as explicit collection aliases while generation and apply adopt the
# shared scope values in their own tasks.
COLLECT_SCOPE_NORMAL_ONLY = SCOPE_NORMAL_ONLY
COLLECT_SCOPE_ALL = SCOPE_ALL
_WHITESPACE_RE = re.compile(r"\s+")
_NOTE_LIMIT = 2000
@@ -93,6 +101,19 @@ def partition_tasks(tasks) -> dict:
return grouped
def normalize_collect_scope(value) -> str:
value = str(value or "").strip().lower()
return SCOPE_ALL if value == SCOPE_ALL else SCOPE_NORMAL_ONLY
def should_collect_content(status, scope) -> bool:
return normalize_collect_scope(scope) == COLLECT_SCOPE_ALL or is_normal(status)
def collect_skip_reason(status) -> str:
return f"{status_label(status)},按本轮范围略过"
def _alert_note(title: str, description: str) -> str:
parts = []
if title: