Fix patrol delete condition handling

This commit is contained in:
koko
2026-05-01 22:36:37 +08:00
parent 28a752265c
commit 74a7cd22b0
14 changed files with 769 additions and 26 deletions

View File

@@ -4,7 +4,7 @@ import time
from copy import deepcopy
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional
from typing import Any, Dict, Iterable, List, Optional, Set
import requests
from loguru import logger
@@ -38,6 +38,19 @@ def _load_patrol_delete_script(name: str) -> str:
return (SCRIPT_DIR / name).read_text(encoding="utf-8")
def _new_trace_id(prefix: str = "trace") -> str:
return f"{prefix}-{int(time.time() * 1000)}"
def _safe_log_text(value: Any, limit: int = 2000) -> str:
try:
text = json.dumps(value, ensure_ascii=False) if isinstance(value, (dict, list)) else str(value)
except Exception:
text = repr(value)
text = re.sub(r"\s+", " ", text).strip()
return text if len(text) <= limit else text[: limit - 3] + "..."
LISTING_STATUS_DROPDOWN_OPEN_SCRIPT = _load_patrol_delete_script("listing_status_dropdown_open.js")
LISTING_STATUS_DROPDOWN_OPTIONS_SCRIPT = _load_patrol_delete_script("listing_status_dropdown_options.js")
LISTING_STATUS_DROPDOWN_CLOSE_SCRIPT = _load_patrol_delete_script("listing_status_dropdown_close.js")
@@ -49,6 +62,9 @@ DELETE_CONFIRM_MODAL_CLICK_SCRIPT = _load_patrol_delete_script("delete_confirm_m
DELETE_SUCCESS_MESSAGES_SCRIPT = _load_patrol_delete_script("delete_success_messages.js")
COMPLETE_DRAFT_QUICK_VIEW_TAGS_SCRIPT = _load_patrol_delete_script("complete_draft_quick_view_tags.js")
COMPLETE_DRAFT_QUICK_VIEW_DIAGNOSTIC_SCRIPT = _load_patrol_delete_script("complete_draft_quick_view_diagnostic.js")
COMPLETE_DRAFT_SELECT_TAG_SCRIPT = _load_patrol_delete_script("complete_draft_select_tag.js")
COMPLETE_DRAFT_SELECT_ALL_SCRIPT = _load_patrol_delete_script("complete_draft_select_all.js")
COMPLETE_DRAFT_DELETE_SELECTED_SCRIPT = _load_patrol_delete_script("complete_draft_delete_selected.js")
RECOMMENDED_OFFER_PERCENTAGE_OPEN_SCRIPT = _load_patrol_delete_script("recommended_offer_percentage_open.js")
RECOMMENDED_OFFER_PERCENTAGE_ROWS_SCRIPT = _load_patrol_delete_script("recommended_offer_percentage_rows.js")
RECOMMENDED_OFFER_PERCENTAGE_DIAGNOSTIC_SCRIPT = _load_patrol_delete_script("recommended_offer_percentage_diagnostic.js")
@@ -65,6 +81,10 @@ DELETE_SUCCESS_MESSAGE_PARTS = (
re.compile(r"\d+\s*个商品(?:已删除|已经删除)"),
re.compile(r"所[作做]更改需要\s*15\s*分钟才会显示在商品详情页面上"),
)
COMPLETE_DRAFT_DELETE_SUCCESS_PATTERNS = (
re.compile(r"商品信息草稿(?:已删除|已经删除)"),
re.compile(r"已删除\s*\d*\s*个?\s*商品信息草稿"),
)
LISTING_STATUS_DELETE_WHITELIST = {
"全部",
"定价问题",
@@ -153,6 +173,229 @@ class InventoryManage(AmamzonBase):
raise RuntimeError(f"补全草稿快速查看已打开但未读取到标签: {open_result}")
return parsed_rows
def delete_complete_drafts_from_non_whitelisted_tags(
self,
shop_name: str,
country: str,
max_delete_operations: Optional[int] = None,
trace_id: str = "",
allowed_statuses: Optional[Iterable[str]] = None,
) -> Dict[str, Any]:
"""按补全草稿快速查看的准确标签数量,删除非白名单标签下方商品。"""
trace_id = trace_id or _new_trace_id("draft")
logger.info(
f"trace_id={trace_id} 开始删除非白名单补全草稿: "
f"shop={shop_name}, country={country}, max_delete_operations={max_delete_operations}, "
f"allowed_statuses={list(allowed_statuses) if allowed_statuses is not None else None}"
)
initial_tags = self.get_complete_draft_quick_view_tags(shop_name, country)
delete_candidates = self._build_deletable_complete_draft_tag_rows(initial_tags, allowed_statuses)
results: List[Dict[str, Any]] = []
delete_counts: Dict[str, int] = {}
total_deleted = 0
stop_reason = "no-candidates"
if not delete_candidates:
logger.info(f"trace_id={trace_id} 补全草稿没有可删除标签: shop={shop_name}, country={country}")
return {
"shopName": shop_name,
"country": country,
"draftRows": initial_tags,
"deleteCandidates": delete_candidates,
"results": results,
"deleteCounts": delete_counts,
"totalDeleted": total_deleted,
"stopReason": stop_reason,
}
stop_reason = "processed-candidates"
for candidate in delete_candidates:
tag = candidate["tag"]
quantity = candidate["quantity"]
canonical_tag = candidate["canonicalTag"]
logger.info(
f"trace_id={trace_id} 开始删除补全草稿标签下方商品: "
f"shop={shop_name}, country={country}, tag={tag}, quantity={quantity}"
)
self.select_complete_draft_tag(tag, trace_id=trace_id)
deleted_for_tag = 0
while quantity <= 0 or deleted_for_tag < quantity:
remaining_limit = None
if max_delete_operations is not None:
remaining_limit = max(0, max_delete_operations - total_deleted)
if remaining_limit <= 0:
stop_reason = "max-delete-operations"
break
select_result = self._select_all_complete_draft_rows(
tag,
trace_id=trace_id,
max_select_count=remaining_limit,
)
selected_count = int(select_result.get("selectedCount") or select_result.get("rowCount") or 0)
if select_result.get("empty"):
logger.info(
f"trace_id={trace_id} 补全草稿标签下方已无商品,跳过删除: "
f"shop={shop_name}, country={country}, tag={tag}"
)
break
if selected_count <= 0:
logger.info(
f"trace_id={trace_id} 补全草稿标签下方没有可删除商品: "
f"shop={shop_name}, country={country}, tag={tag}"
)
break
delete_result = self._click_complete_draft_delete_selected(tag, trace_id=trace_id)
self._confirm_delete_modal(tag, trace_id=trace_id)
self._wait_complete_draft_rows_after_delete(tag, select_result, trace_id=trace_id)
deleted_count = min(selected_count, quantity - deleted_for_tag) if quantity > 0 else selected_count
deleted_for_tag += deleted_count
total_deleted += deleted_count
delete_counts[canonical_tag] = delete_counts.get(canonical_tag, 0) + deleted_count
results.append(
{
"tag": tag,
"canonicalTag": canonical_tag,
"tagQuantity": quantity,
"deletedCount": deleted_count,
"selectResult": select_result,
"deleteResult": delete_result,
}
)
logger.info(
f"trace_id={trace_id} 补全草稿标签删除成功: "
f"shop={shop_name}, country={country}, tag={tag}, "
f"deleted_count={deleted_count}, tag_deleted={deleted_for_tag}, total_deleted={total_deleted}"
)
if max_delete_operations is not None and total_deleted >= max_delete_operations:
stop_reason = "max-delete-operations"
break
if quantity > 0 and deleted_for_tag >= quantity:
break
if stop_reason == "max-delete-operations":
break
final_tags = self.get_complete_draft_quick_view_tags(shop_name, country)
return {
"shopName": shop_name,
"country": country,
"draftRows": final_tags,
"deleteCandidates": delete_candidates,
"results": results,
"deleteCounts": delete_counts,
"totalDeleted": total_deleted,
"stopReason": stop_reason,
}
def select_complete_draft_tag(self, tag: str, trace_id: str = "") -> Dict[str, Any]:
normalized_tag = self._normalize_option_text(tag)
if not normalized_tag:
raise RuntimeError("补全草稿标签为空,无法执行筛选")
trace_id = trace_id or _new_trace_id("draft")
result = self._run_js(COMPLETE_DRAFT_SELECT_TAG_SCRIPT, {"tag": normalized_tag}) or {}
if not result.get("ok"):
logger.info(
f"trace_id={trace_id} 补全草稿快速查看标签未命中: "
f"tag={normalized_tag}, result={_safe_log_text(result)}"
)
raise RuntimeError(f"未找到补全草稿标签[{normalized_tag}]: {result}")
time.sleep(LISTING_STATUS_SELECT_DELAY)
return result
def _select_all_complete_draft_rows(
self,
tag: str,
trace_id: str = "",
max_select_count: Optional[int] = None,
) -> Dict[str, Any]:
trace_id = trace_id or _new_trace_id("draft")
deadline = time.time() + INVENTORY_ROW_TIMEOUT
result: Dict[str, Any] = {}
saw_empty = False
while time.time() < deadline:
payload = {"maxSelectCount": max_select_count} if max_select_count is not None else None
result = self._run_js(COMPLETE_DRAFT_SELECT_ALL_SCRIPT, payload) if payload else self._run_js(COMPLETE_DRAFT_SELECT_ALL_SCRIPT)
result = result or {}
if result.get("ok"):
time.sleep(0.5)
logger.info(f"trace_id={trace_id} 补全草稿已全选: tag={tag}, result={_safe_log_text(result)}")
return result
if result.get("reason") == "no complete draft rows":
saw_empty = True
time.sleep(0.5)
continue
if result.get("reason") and result.get("reason") != "no complete draft rows":
break
time.sleep(0.5)
if not result.get("ok"):
if saw_empty and result.get("reason") == "no complete draft rows":
logger.info(f"trace_id={trace_id} 补全草稿标签下方无商品行: tag={tag}, result={_safe_log_text(result)}")
return {**result, "ok": True, "empty": True, "selectedCount": 0, "rowCount": 0}
logger.info(
f"trace_id={trace_id} 补全草稿全选失败: tag={tag}, result={_safe_log_text(result)}"
)
raise RuntimeError(f"补全草稿标签[{tag}]全选失败: {result}")
return result
def _click_complete_draft_delete_selected(self, tag: str, trace_id: str = "") -> Dict[str, Any]:
trace_id = trace_id or _new_trace_id("draft")
result: Dict[str, Any] = {}
for attempt in range(8):
result = self._run_js(COMPLETE_DRAFT_DELETE_SELECTED_SCRIPT) or {}
if result.get("ok"):
break
if result.get("menuOpened"):
logger.info(
f"trace_id={trace_id} 补全草稿选择组操作菜单已打开,等待删除选项出现: "
f"tag={tag}, attempt={attempt + 1}, result={_safe_log_text(result)}"
)
time.sleep(0.8)
continue
break
if not result.get("ok"):
logger.info(
f"trace_id={trace_id} 补全草稿点击删除失败: tag={tag}, result={_safe_log_text(result)}"
)
raise RuntimeError(f"补全草稿标签[{tag}]点击删除失败: {result}")
time.sleep(0.5)
logger.info(f"trace_id={trace_id} 补全草稿删除按钮点击成功: tag={tag}, result={_safe_log_text(result)}")
return result
def _wait_complete_draft_rows_after_delete(
self,
tag: str,
before_result: Dict[str, Any],
trace_id: str = "",
) -> None:
trace_id = trace_id or _new_trace_id("draft")
before_count = int(before_result.get("rowCount") or 0)
deadline = time.time() + DELETE_SUCCESS_TIMEOUT
last_result: Dict[str, Any] = {}
while time.time() < deadline:
time.sleep(0.5)
result = self._run_js(COMPLETE_DRAFT_SELECT_ALL_SCRIPT, {"probeOnly": True}) or {}
last_result = result
row_count = int(result.get("rowCount") or 0)
if row_count < before_count or row_count == 0:
logger.info(
f"trace_id={trace_id} 补全草稿删除后行数已变化: "
f"tag={tag}, before={before_count}, after={row_count}"
)
return
for text in self._collect_delete_success_messages():
if self._is_complete_draft_delete_success_message(text) or self._is_delete_success_message(text):
logger.info(f"trace_id={trace_id} 补全草稿删除读取到成功提示: tag={tag}, message={_safe_log_text(text)}")
return
logger.info(
f"trace_id={trace_id} 补全草稿删除后未确认行数变化: "
f"tag={tag}, before={before_count}, last_result={_safe_log_text(last_result)}"
)
raise RuntimeError("补全草稿删除后未确认行数变化")
def open_recommended_offer_percentage_detail(self) -> Dict[str, Any]:
"""打开 Seller Central 首页中的推荐报价百分比明细。"""
if self.tab is None:
@@ -206,13 +449,15 @@ class InventoryManage(AmamzonBase):
shop_name: str,
country: str,
max_delete_operations: Optional[int] = None,
allowed_statuses: Optional[Iterable[str]] = None,
) -> Dict[str, Any]:
"""循环删除非白名单分类商品,直到当前国家没有可删项。"""
logger.info(
"开始批量删除非白名单商品: shop={}, country={}, max_delete_operations={}",
"开始批量删除非白名单商品: shop={}, country={}, max_delete_operations={}, allowed_statuses={}",
shop_name,
country,
max_delete_operations,
list(allowed_statuses) if allowed_statuses is not None else None,
)
all_results: List[Dict[str, Any]] = []
delete_counts: Dict[str, int] = {}
@@ -222,7 +467,7 @@ class InventoryManage(AmamzonBase):
stop_reason = "no-candidates"
latest_status_rows = self._read_listing_status_rows_for_delete(shop_name, country)
latest_delete_candidates = self._build_deletable_status_rows(latest_status_rows)
latest_delete_candidates = self._build_deletable_status_rows(latest_status_rows, allowed_statuses)
if not latest_delete_candidates:
logger.info("未找到可删除商品分类: shop={}, country={}", shop_name, country)
final_status_rows = latest_status_rows
@@ -456,7 +701,7 @@ class InventoryManage(AmamzonBase):
text = ""
return {"sku": sku, "rawText": text[:400]}
def _confirm_delete_modal(self, status: str) -> None:
def _confirm_delete_modal(self, status: str, trace_id: str = "") -> None:
js_result = self._click_confirm_delete_modal_with_js()
if js_result.get("ok"):
logger.info("已通过 JS 确认删除弹窗: status={}, result={}", status, js_result)
@@ -544,6 +789,11 @@ class InventoryManage(AmamzonBase):
normalized = re.sub(r"\s+", "", text or "")
return all(pattern.search(normalized) for pattern in DELETE_SUCCESS_MESSAGE_PARTS)
@staticmethod
def _is_complete_draft_delete_success_message(text: str) -> bool:
normalized = re.sub(r"\s+", "", text or "")
return any(pattern.search(normalized) for pattern in COMPLETE_DRAFT_DELETE_SUCCESS_PATTERNS)
@staticmethod
def _extract_delete_success_count(text: str) -> int:
match = re.search(r"(\d+)\s*个商品(?:已删除|已经删除)", text or "")
@@ -733,7 +983,12 @@ class InventoryManage(AmamzonBase):
]
@classmethod
def _build_deletable_status_rows(cls, rows: Iterable[Dict[str, Any]]) -> List[Dict[str, Any]]:
def _build_deletable_status_rows(
cls,
rows: Iterable[Dict[str, Any]],
allowed_statuses: Optional[Iterable[str]] = None,
) -> List[Dict[str, Any]]:
allowed_status_set = cls._normalize_allowed_status_set(allowed_statuses)
candidates = []
for row in rows:
status = cls._normalize_option_text(row.get("status") or "")
@@ -743,6 +998,8 @@ class InventoryManage(AmamzonBase):
quantity_text = cls._normalize_quantity_text(row.get("quantity"))
quantity = int(quantity_text) if quantity_text else 0
canonical_status = cls._canonical_listing_status(status)
if allowed_status_set is not None and canonical_status not in allowed_status_set:
continue
if not quantity_text or quantity <= 0 or canonical_status in LISTING_STATUS_DELETE_WHITELIST:
continue
@@ -755,6 +1012,37 @@ class InventoryManage(AmamzonBase):
)
return candidates
@classmethod
def _build_deletable_complete_draft_tag_rows(
cls,
rows: Iterable[Dict[str, Any]],
allowed_statuses: Optional[Iterable[str]] = None,
) -> List[Dict[str, Any]]:
allowed_status_set = cls._normalize_allowed_status_set(allowed_statuses)
complete_draft_enabled = bool(
allowed_status_set and {"补全草稿", "商品信息草稿"}.intersection(allowed_status_set)
)
candidates = []
for row in rows:
tag = cls._normalize_option_text(row.get("tag") or row.get("status") or "")
if not tag:
continue
quantity_text = cls._normalize_quantity_text(row.get("quantity"))
quantity = int(quantity_text) if quantity_text else 0
canonical_tag = cls._canonical_listing_status(tag)
if allowed_status_set is not None and not complete_draft_enabled and canonical_tag not in allowed_status_set:
continue
if not quantity_text or quantity <= 0 or canonical_tag in LISTING_STATUS_DELETE_WHITELIST:
continue
candidates.append(
{
"tag": tag,
"canonicalTag": canonical_tag,
"quantity": quantity,
}
)
return candidates
@staticmethod
def _format_deletable_candidates(candidates: Iterable[Dict[str, Any]]) -> str:
summary = []
@@ -769,6 +1057,21 @@ class InventoryManage(AmamzonBase):
normalized = cls._normalize_option_text(status)
return LISTING_STATUS_COMPATIBILITY_MAP.get(normalized, normalized)
@classmethod
def _normalize_allowed_status_set(cls, statuses: Optional[Iterable[str]]) -> Optional[Set[str]]:
if statuses is None:
return None
allowed = set()
for status in statuses:
text = cls._normalize_option_text(str(status or ""))
if not text:
continue
for part in re.split(r"[,;;、\r\n]+", text):
normalized = cls._normalize_option_text(part)
if normalized:
allowed.add(cls._canonical_listing_status(normalized))
return allowed
@staticmethod
def _normalize_option_text(text: str) -> str:
return re.sub(r"\s+", " ", text).strip()
@@ -1249,6 +1552,7 @@ class PatrolDeleteTask:
items = data.get("items", [])
country_sections = data.get("country_sections", [])
cart_ratios = data.get("cart_ratios", [])
delete_conditions = self._resolve_delete_condition_texts(data)
if not task_id:
self.log("任务ID为空跳过", "WARNING")
@@ -1274,7 +1578,10 @@ class PatrolDeleteTask:
"stop_requested": False,
}
self.log(f"开始处理巡店删除任务 {task_id},共 {len(items)} 个店铺,{total_countries} 个国家")
self.log(
f"开始处理巡店删除任务 {task_id},共 {len(items)} 个店铺,{total_countries} 个国家,"
f"删除条件={delete_conditions if delete_conditions is not None else '未指定'}"
)
for index, shop_item in enumerate(items, 1):
if runing_task[task_id].get("stop_requested", False):
self.log(f"检测到任务 {task_id} 的暂停请求,停止处理", "WARNING")
@@ -1283,7 +1590,7 @@ class PatrolDeleteTask:
shop_name = shop_item.get("shopName", "未知店铺")
self.log(f"[{index}/{len(items)}] 开始处理店铺: {shop_name}")
self.process_shop(task_id, shop_item, country_sections, cart_ratios)
self.process_shop(task_id, shop_item, country_sections, cart_ratios, delete_conditions)
runing_task[task_id]["processed_shops"] += 1
if runing_task[task_id].get("stop_requested", False):
@@ -1309,6 +1616,7 @@ class PatrolDeleteTask:
shop_item: Dict[str, Any],
country_sections: List[Dict[str, Any]],
cart_ratios: List[Dict[str, Any]],
delete_conditions: Optional[List[str]] = None,
):
"""处理单个店铺。"""
from config import runing_shop, runing_task
@@ -1334,7 +1642,10 @@ class PatrolDeleteTask:
country_names = self._ordered_countries(payload_country_sections)
aggregated_sections = self._build_country_section_templates(payload_country_sections)
aggregated_cart_ratios = self._build_cart_ratio_templates(country_names, payload_cart_ratios)
self.log(f"店铺 {shop_name} 待处理国家: {', '.join(country_names)}")
self.log(
f"店铺 {shop_name} 待处理国家: {', '.join(country_names)}"
f"删除条件={delete_conditions if delete_conditions is not None else '未指定'}"
)
driver: Optional[InventoryManage] = None
requires_browser_reset = False
@@ -1362,6 +1673,7 @@ class PatrolDeleteTask:
task_id=task_id,
shop_name=shop_name,
country_name=country_name,
delete_conditions=delete_conditions,
)
country_results.append(country_result)
aggregated_sections[country_name] = country_result["countrySection"]
@@ -1472,6 +1784,7 @@ class PatrolDeleteTask:
task_id: int,
shop_name: str,
country_name: str,
delete_conditions: Optional[List[str]] = None,
) -> Dict[str, Any]:
from config import runing_task
@@ -1487,7 +1800,11 @@ class PatrolDeleteTask:
driver.switch_to_manage_inventory()
self.log(f"国家 {country_name} 已进入管理所有库存页面,开始删除非白名单商品")
delete_result = driver.delete_all_listings_from_non_whitelisted_statuses(shop_name, country_name)
delete_result = driver.delete_all_listings_from_non_whitelisted_statuses(
shop_name,
country_name,
allowed_statuses=delete_conditions,
)
latest_status_rows = delete_result.get("statusRows") or []
self.log(
f"国家 {country_name} 删除步骤完成deleted={delete_result.get('totalDeleted', 0)}"
@@ -1499,17 +1816,26 @@ class PatrolDeleteTask:
delete_counts=delete_result.get("deleteCounts") or {},
default_process_status="无可删商品" if delete_result.get("totalDeleted", 0) == 0 else "",
)
complete_draft_section = self._read_complete_draft_country_section_safe(driver, shop_name, country_name)
complete_draft_delete_result = self._delete_complete_draft_country_section_safe(
driver,
shop_name,
country_name,
delete_conditions,
)
complete_draft_section = complete_draft_delete_result.get("countrySection")
country_section = self._replace_complete_draft_rows(country_section, complete_draft_section)
cart_ratio = self._read_country_cart_ratio_safe(driver, country_name)
self.log(f"国家 {country_name} 购物车比例读取完成: {cart_ratio.get('ratio', '')}")
total_deleted = int(delete_result.get("totalDeleted") or 0) + int(
complete_draft_delete_result.get("deletedCount") or 0
)
return {
"success": True,
"countrySection": country_section,
"cartRatio": cart_ratio,
"deletedCount": delete_result.get("totalDeleted", 0),
"deletedCount": total_deleted,
"reopenRequired": False,
}
except Exception as exc:
@@ -1566,6 +1892,51 @@ class PatrolDeleteTask:
self.log(f"读取国家 {country_name} 补全草稿失败: {str(draft_exc)}", "WARNING")
return None
def _delete_complete_draft_country_section_safe(
self,
driver: InventoryManage,
shop_name: str,
country_name: str,
delete_conditions: Optional[List[str]] = None,
) -> Dict[str, Any]:
trace_id = _new_trace_id("draft")
try:
self.log(f"trace_id={trace_id} 开始处理国家 {country_name} 补全草稿删除")
delete_result = driver.delete_complete_drafts_from_non_whitelisted_tags(
shop_name,
country_name,
trace_id=trace_id,
allowed_statuses=delete_conditions,
)
rows = []
delete_counts = delete_result.get("deleteCounts") or {}
for item in delete_result.get("draftRows") or []:
status = PatrolDeleteTask._string_result_field(item.get("tag") or item.get("status"))
canonical_status = InventoryManage._canonical_listing_status(status)
deleted_count = int(delete_counts.get(canonical_status) or 0)
rows.append(
{
"type": "completeDraft",
"status": status,
"quantity": InventoryManage._normalize_quantity_text(item.get("quantity")),
"deleteQuantity": str(deleted_count),
}
)
section = {"country": country_name, "rows": rows} if rows else None
self.log(
f"trace_id={trace_id} 国家 {country_name} 补全草稿删除处理完成: "
f"deleted={delete_result.get('totalDeleted', 0)}, rows={rows}"
)
return {
"countrySection": section,
"deletedCount": int(delete_result.get("totalDeleted") or 0),
"deleteResult": delete_result,
}
except Exception as draft_exc:
self.log(f"trace_id={trace_id} 删除国家 {country_name} 补全草稿失败: {str(draft_exc)}", "WARNING")
fallback_section = self._read_complete_draft_country_section_safe(driver, shop_name, country_name)
return {"countrySection": fallback_section, "deletedCount": 0, "error": str(draft_exc)}
def _switch_country_with_retry(self, driver: InventoryManage, country_name: str) -> None:
for retry in range(PRODUCT_TASK_MAX_RETRIES):
try:
@@ -1585,6 +1956,41 @@ class PatrolDeleteTask:
time.sleep(PRODUCT_TASK_RETRY_DELAY)
raise RuntimeError(f"切换国家 {country_name} 失败")
@classmethod
def _resolve_delete_condition_texts(cls, data: Dict[str, Any]) -> Optional[List[str]]:
if "delete_conditions" in data:
raw_conditions = data.get("delete_conditions")
elif "deleteConditions" in data:
raw_conditions = data.get("deleteConditions")
else:
return []
return cls._parse_delete_condition_texts(raw_conditions)
@classmethod
def _parse_delete_condition_texts(cls, raw_conditions: Any) -> List[str]:
if raw_conditions is None:
return []
if not isinstance(raw_conditions, list):
raw_conditions = [raw_conditions]
texts: List[str] = []
for item in raw_conditions:
text = ""
if isinstance(item, dict):
for key in ("conditionText", "condition_text", "status", "text", "label", "name"):
value = item.get(key)
if value:
text = str(value)
break
elif item is not None:
text = str(item)
for part in re.split(r"[,;;、\r\n]+", text):
normalized = InventoryManage._normalize_option_text(part)
if normalized and normalized not in texts:
texts.append(normalized)
return texts
def post_result(
self,
task_id: int,