fix: capture invalid product toast and close failed tabs

This commit is contained in:
chengma
2026-07-02 21:46:32 +08:00
parent f3e0956cf6
commit b9f614b8e7
11 changed files with 378 additions and 28 deletions
+163 -9
View File
@@ -192,6 +192,36 @@ JS_TOASTS = (
"return JSON.stringify(t.slice(0,5));})()"
)
JS_TOAST_OBSERVER_SOURCE = (
"(function(){"
"if(window.__cmshopee_toast_observer_installed)return true;"
"window.__cmshopee_toast_observer_installed=true;"
"window.__cmshopee_toasts=window.__cmshopee_toasts||[];"
"function text(e){return ((e&&e.innerText)||(e&&e.textContent)||'').trim();}"
"function visible(e){if(!e)return false;var s=getComputedStyle(e);var r=e.getBoundingClientRect();return s.display!=='none'&&s.visibility!=='hidden'&&r.width>0&&r.height>0;}"
"function rootOf(e){return e.closest?e.closest('.eds-toast,.eds-toasts,[role=alert],[class*=toast],[class*=Toast],[class*=message],[class*=Message],[class*=notice]')||e:e;}"
"function record(e){var root=rootOf(e);var value=text(root)||text(e);if(!value)return;window.__cmshopee_toasts.push({text:value,html:((root.outerHTML||e.outerHTML)||'').slice(0,2000),url:location.href,visible:visible(root)||visible(e),created_at:(new Date()).toISOString()});}"
"function scan(){try{Array.from(document.querySelectorAll('.eds-toasts .eds-toast,.eds-toast,.eds-toast__content,[role=alert],[class*=toast],[class*=Toast],[class*=message],[class*=Message],[class*=notice]')).forEach(record);window.__cmshopee_toasts=window.__cmshopee_toasts.slice(-20);}catch(e){}}"
"try{if(document.documentElement||document.body){var mo=new MutationObserver(function(){scan();});mo.observe(document.documentElement||document.body,{childList:true,subtree:true,characterData:true,attributes:true,attributeFilter:['style','class']});window.__cmshopee_toast_observer=mo;}}catch(e){}"
"scan();return true;"
"})()"
)
JS_INSTALL_TOAST_OBSERVER = JS_TOAST_OBSERVER_SOURCE
JS_PAGE_TOASTS = (
"(function(){"
"function text(e){return ((e&&e.innerText)||(e&&e.textContent)||'').trim();}"
"function visible(e){if(!e)return false;var s=getComputedStyle(e);var r=e.getBoundingClientRect();return s.display!=='none'&&s.visibility!=='hidden'&&r.width>0&&r.height>0;}"
"function rootOf(e){return e.closest?e.closest('.eds-toast,.eds-toasts,[role=alert],[class*=toast],[class*=Toast],[class*=message],[class*=Message],[class*=notice]')||e:e;}"
"function record(e){var root=rootOf(e);var value=text(root)||text(e);if(!value)return null;return {text:value,html:((root.outerHTML||e.outerHTML)||'').slice(0,2000),url:location.href,visible:visible(root)||visible(e),created_at:(new Date()).toISOString()};}"
"var cached=(window.__cmshopee_toasts||[]).slice(-10);"
"var current=Array.from(document.querySelectorAll('.eds-toasts .eds-toast,.eds-toast,.eds-toast__content,[role=alert],[class*=toast],[class*=Toast],[class*=message],[class*=Message],[class*=notice]')).map(record).filter(Boolean);"
"var seen={};var out=[];cached.concat(current).forEach(function(item){var key=[item.text,item.html,item.url].join('|');if(seen[key])return;seen[key]=true;out.push(item);});"
"return JSON.stringify(out.slice(-10));"
"})()"
)
JS_POST_UPDATE_STATE = (
"(function(){"
"function visible(e){if(!e)return false;var r=e.getBoundingClientRect();var s=getComputedStyle(e);"
@@ -314,11 +344,112 @@ def _is_login_url(url):
def _json_value(cdp, expr, default=None):
raw = cdp.val(expr)
if not raw:
if raw is None or raw == "":
return default
if isinstance(raw, (dict, list)):
return raw
return json.loads(raw)
PRODUCT_UNAVAILABLE_TEXT_MARKERS = (
"please input correct product id",
"商品失效",
"商品不存在",
"商品已删除",
"商品已下架",
"無效商品",
"无效商品",
"無效的商品",
"无效的商品",
"無權限",
"无权限",
"沒有權限",
"没有权限",
"無法查看商品",
"无法查看商品",
"product not found",
"item not found",
"not exist",
"not found",
"invalid product",
"invalid item",
)
def install_toast_observer(cdp) -> None:
try:
cdp.send("Page.addScriptToEvaluateOnNewDocument", {"source": JS_TOAST_OBSERVER_SOURCE})
except Exception:
pass
try:
cdp.val(JS_INSTALL_TOAST_OBSERVER)
except Exception:
pass
def read_page_toasts(cdp):
try:
values = _json_value(cdp, JS_PAGE_TOASTS, default=[])
except Exception:
return []
if not isinstance(values, list):
return []
toasts = []
for entry in values:
if isinstance(entry, str):
text = entry.strip()
if text:
toasts.append({"text": text, "html": "", "url": "", "visible": False, "created_at": ""})
continue
if not isinstance(entry, dict):
continue
text = str(entry.get("text") or "").strip()
if not text:
continue
toasts.append(
{
"text": text,
"html": str(entry.get("html") or "")[:2000],
"url": str(entry.get("url") or ""),
"visible": bool(entry.get("visible")),
"created_at": str(entry.get("created_at") or ""),
}
)
return toasts[-10:]
def is_product_unavailable_error(text):
value = str(text or "").strip().lower()
return bool(value) and any(marker.lower() in value for marker in PRODUCT_UNAVAILABLE_TEXT_MARKERS)
def product_unavailable_error_message(text):
value = str(text or "").strip()
if value.startswith("商品失效"):
return value
return f"商品失效:{value or '商品详情页无法加载'}"
def _product_unavailable_toast_text(toasts):
for toast in reversed(toasts or []):
text = str(toast.get("text") or "").strip()
if is_product_unavailable_error(text):
return text
return None
def _open_product_ready_error(cdp, fallback):
toasts = read_page_toasts(cdp)
invalid_text = _product_unavailable_toast_text(toasts)
if invalid_text:
return product_unavailable_error_message(invalid_text)
for toast in reversed(toasts):
text = str(toast.get("text") or "").strip()
if text:
return f"{fallback}:{text}"
return fallback
def _title_state(cdp):
return _json_value(cdp, JS_TITLE_STATE, default={}) or {}
@@ -506,8 +637,11 @@ def _wait_ready(cdp, timeout=60):
return True
except Exception:
pass
invalid_text = _product_unavailable_toast_text(read_page_toasts(cdp))
if invalid_text:
raise EditorError(product_unavailable_error_message(invalid_text))
time.sleep(1)
raise TimeoutError("等待 Shopee 商品编辑器就绪超时")
raise EditorError(_open_product_ready_error(cdp, "等待 Shopee 商品编辑器就绪超时"))
def _ensure_page_domains(cdp):
@@ -598,6 +732,20 @@ def is_logged_in(account) -> bool:
return bool(login_status(account).get("logged_in"))
def _close_open_product_failure(cdp):
target_id = getattr(cdp, "target_id", None)
created_by_app = bool(getattr(cdp, "created_by_app", False))
host = getattr(cdp, "cdp_host", None)
try:
cdp.close()
finally:
if created_by_app and target_id:
try:
close_tab(target_id, host=host)
except Exception:
pass
def open_product(account, item_id, on_step=None) -> CDP:
"""Open or reuse a product edit tab, navigate to a clean edit URL, and wait ready."""
@@ -614,15 +762,21 @@ def open_product(account, item_id, on_step=None) -> CDP:
cdp.target_id = tab.get("id")
cdp.created_by_app = created_by_app
cdp.cdp_host = host
_ensure_page_domains(cdp)
try:
cdp.send("Page.bringToFront")
_ensure_page_domains(cdp)
install_toast_observer(cdp)
try:
cdp.send("Page.bringToFront")
except Exception:
pass
cdp.send("Page.navigate", {"url": url})
install_toast_observer(cdp)
_notify_collect_step(on_step, "wait_ready")
_wait_ready(cdp)
return cdp
except Exception:
pass
cdp.send("Page.navigate", {"url": url})
_notify_collect_step(on_step, "wait_ready")
_wait_ready(cdp)
return cdp
_close_open_product_failure(cdp)
raise
def read_title(cdp) -> str:
+29 -2
View File
@@ -21,6 +21,21 @@ class TaskTableModel(QAbstractTableModel):
"skipped": "略过",
"cancelled": "已取消",
}
PRODUCT_UNAVAILABLE_MARKERS = (
"商品失效",
"please input correct product id",
"商品不存在",
"商品已删除",
"商品已下架",
"無效商品",
"无效商品",
"無權限",
"无权限",
"product not found",
"item not found",
"invalid product",
"invalid item",
)
def __init__(self, parent=None):
super().__init__(parent)
@@ -72,8 +87,12 @@ class TaskTableModel(QAbstractTableModel):
return self._display_value(task, index.column())
if role == Qt.ForegroundRole and index.column() == 3:
return self._stage_color(task)
if role == Qt.ToolTipRole and self.is_unmatched(task):
return "别名未匹配账号,采集时将略过"
if role == Qt.ToolTipRole:
if self.is_unmatched(task):
return "别名未匹配账号,采集时将略过"
error = getattr(task, "last_error", "") or ""
if error:
return str(error)
return None
def flags(self, index):
@@ -98,9 +117,17 @@ class TaskTableModel(QAbstractTableModel):
return account.account_name
return task.account_name or ""
def _is_product_unavailable(self, task) -> bool:
if getattr(task, "status", "") != "failed":
return False
error = str(getattr(task, "last_error", "") or "").lower()
return bool(error) and any(marker.lower() in error for marker in self.PRODUCT_UNAVAILABLE_MARKERS)
def _stage_text(self, task) -> str:
if self.is_unmatched(task):
return "略过"
if self._is_product_unavailable(task):
return "商品失效"
if task.status in self.STATUS_TEXT and task.status != "pending":
return self.STATUS_TEXT[task.status]
return self.STAGE_TEXT.get(task.stage, task.stage)