fix: capture invalid product toast and close failed tabs
This commit is contained in:
+163
-9
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user