fix: stabilize Shopee cover replacement
This commit is contained in:
+107
-69
@@ -5,6 +5,7 @@ import os
|
||||
import time
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from . import image_paths
|
||||
from .cdp import CDP, close_tab, create_tab, create_tab_info, find_product_tab, http_get
|
||||
|
||||
|
||||
@@ -55,8 +56,8 @@ JS_UPLOAD_STATE = (
|
||||
"var upload=manager?manager.querySelector('.shopee-image-manager__upload input[type=file]'):null;"
|
||||
"var uploadBox=manager?manager.querySelector('.shopee-image-manager__upload,[class*=image-manager__upload]'):null;"
|
||||
"var busy=manager?[].slice.call(manager.querySelectorAll('[class*=loading],[class*=Loading],[class*=spinner],[class*=Spinner],[class*=progress],[class*=Progress],[class*=uploading],[class*=Uploading]')).filter(visible):[];"
|
||||
"var errRe=/(失敗|失败|錯誤|错误|不支援|不支持|格式|大小|尺寸|超過|超过|error|fail|invalid|unsupported)/i;"
|
||||
"var uploadRe=/(圖片|图片|封面|照片|相片|圖像|图像|image|photo|cover|upload|上傳|上传|檔案|文件|file|格式|大小|尺寸|像素|解析度|分辨率|超過|超过)/i;"
|
||||
"var errRe=/(失敗|失败|錯誤|错误|不支援|不支持|格式|大小|尺寸|超過|超过|重複|重复|duplicate|error|fail|invalid|unsupported)/i;"
|
||||
"var uploadRe=/(圖片|图片|封面|照片|相片|圖像|图像|image|photo|cover|upload|上傳|上传|檔案|文件|file|格式|大小|尺寸|像素|解析度|分辨率|超過|超过|重複|重复|duplicate)/i;"
|
||||
"var texts=manager?[].slice.call(manager.querySelectorAll('*')).filter(visible).map(text).filter(Boolean):[];"
|
||||
"var errors=texts.filter(function(t){return errRe.test(t);}).slice(0,8);"
|
||||
"var roots=[].slice.call(document.querySelectorAll('.eds-modal__content,.eds-modal__box,[role=dialog]')).filter(visible);"
|
||||
@@ -84,6 +85,19 @@ JS_UPLOAD_STATE = (
|
||||
"});})()"
|
||||
)
|
||||
|
||||
JS_CLICK_UPLOAD_TILE = (
|
||||
"(function(){"
|
||||
"function visible(e){if(!e)return false;var r=e.getBoundingClientRect();var s=getComputedStyle(e);"
|
||||
"return r.width>0&&r.height>0&&s.visibility!=='hidden'&&s.display!=='none';}"
|
||||
"var manager=document.querySelector('.shopee-image-manager');"
|
||||
"var box=manager?manager.querySelector('.shopee-image-manager__upload,[class*=image-manager__upload]'):null;"
|
||||
"if(!box)return JSON.stringify({clicked:false,reason:'NO_UPLOAD_TILE'});"
|
||||
"if(!visible(box))return JSON.stringify({clicked:false,reason:'UPLOAD_TILE_HIDDEN'});"
|
||||
"box.scrollIntoView({block:'center',inline:'center'});"
|
||||
"box.click();"
|
||||
"return JSON.stringify({clicked:true,reason:null});"
|
||||
"})()"
|
||||
)
|
||||
JS_TITLE_STATE = (
|
||||
"(function(){"
|
||||
f"var r=document.evaluate({json.dumps(TITLE_XPATH)},document,null,"
|
||||
@@ -317,6 +331,14 @@ def _upload_state(cdp):
|
||||
return _json_value(cdp, JS_UPLOAD_STATE, default={}) or {}
|
||||
|
||||
|
||||
def _click_upload_tile(cdp):
|
||||
return _json_value(
|
||||
cdp,
|
||||
JS_CLICK_UPLOAD_TILE,
|
||||
default={"clicked": False, "reason": "NO_UPLOAD_TILE"},
|
||||
) or {"clicked": False, "reason": "NO_UPLOAD_TILE"}
|
||||
|
||||
|
||||
def _post_update_state(cdp):
|
||||
return _json_value(cdp, JS_POST_UPDATE_STATE, default=None) or {}
|
||||
|
||||
@@ -431,9 +453,24 @@ def _wait_image_manager_stable(
|
||||
}
|
||||
|
||||
|
||||
def _has_duplicate_upload_error(state):
|
||||
texts = []
|
||||
for key in ("errors", "error_toasts", "toasts", "page_error_toasts"):
|
||||
values = (state or {}).get(key) or []
|
||||
texts.extend(str(value) for value in values)
|
||||
return any(
|
||||
token in text.lower()
|
||||
for text in texts
|
||||
for token in ("重複", "重复", "duplicate")
|
||||
)
|
||||
|
||||
|
||||
def _cover_upload_error_message(result):
|
||||
reason = result.get("reason") or "COVER_UPDATE_FAILED"
|
||||
state = result.get("upload_state") or {}
|
||||
if reason == "UPLOAD_DUPLICATE_IMAGE":
|
||||
details = state.get("errors") or state.get("error_toasts") or state.get("toasts") or state.get("page_error_toasts") or []
|
||||
return "新封面与现有商品图片重复:" + ";".join(map(str, details[:3])) if details else "新封面与现有商品图片重复"
|
||||
if reason == "UPLOAD_PAGE_ERROR":
|
||||
details = state.get("errors") or state.get("error_toasts") or state.get("toasts") or []
|
||||
return "新封面上传失败:" + ";".join(map(str, details[:3])) if details else "新封面上传失败"
|
||||
@@ -443,6 +480,8 @@ def _cover_upload_error_message(result):
|
||||
return "新封面上传仍在处理中,未取得 Shopee CDN 地址"
|
||||
if reason == "UPLOAD_TIMEOUT":
|
||||
return "新封面上传超时,未取得 Shopee CDN 地址"
|
||||
if reason == "UPLOAD_TILE_NOT_READY":
|
||||
return "新封面上传入口未可点击,未开始上传新封面"
|
||||
if reason == "IMAGE_MANAGER_BUSY":
|
||||
return "商品图片区域仍在加载,未开始上传新封面"
|
||||
if reason == "IMAGE_COUNT_NOT_READY":
|
||||
@@ -634,7 +673,7 @@ def collect(account, task, on_step=None) -> dict:
|
||||
old_cover_src = read_cover_src(cdp)
|
||||
out_path = _get(task, "old_cover_path")
|
||||
if not out_path:
|
||||
out_path = os.path.join(_image_root(account), f"{item_id}_old.jpg")
|
||||
out_path = image_paths.task_image_path("images", task, account, "old")
|
||||
_notify_collect_step(on_step, "download_cover")
|
||||
old_cover_path = download_cover(old_cover_src, out_path)
|
||||
return {
|
||||
@@ -694,8 +733,8 @@ def change_title(cdp, new_title) -> dict:
|
||||
def replace_cover(cdp, image_win_path, old_cover_path=None, timeout=180) -> dict:
|
||||
"""Upload an image and drag it to the first position.
|
||||
|
||||
When the image manager is full, only delete the current first image if the
|
||||
old cover backup from the collect stage exists locally.
|
||||
Cover replacement always deletes the current first Shopee image first, and
|
||||
only proceeds when the old cover backup from the collect stage exists.
|
||||
"""
|
||||
|
||||
image_win_path = os.path.abspath(str(image_win_path))
|
||||
@@ -726,61 +765,52 @@ def replace_cover(cdp, image_win_path, old_cover_path=None, timeout=180) -> dict
|
||||
|
||||
before = stable.get("rects") or _image_rects(cdp)
|
||||
count_before = len(before)
|
||||
delete_result = None
|
||||
if len(before) >= 9:
|
||||
backup_path = _validated_old_cover_backup(old_cover_path)
|
||||
if not backup_path:
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": "OLD_COVER_BACKUP_MISSING",
|
||||
"count_before": count_before,
|
||||
"old_cover_path": old_cover_path,
|
||||
}
|
||||
delete_result = _delete_first_cover(cdp, before)
|
||||
if not delete_result.get("ok"):
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": delete_result.get("reason"),
|
||||
"count_before": count_before,
|
||||
"delete": delete_result,
|
||||
}
|
||||
stable = _wait_image_manager_stable(
|
||||
cdp,
|
||||
expected_count=delete_result.get("count_after"),
|
||||
timeout=30,
|
||||
settle_seconds=2.0,
|
||||
require_upload_input=True,
|
||||
)
|
||||
if not stable.get("ok"):
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": stable.get("reason"),
|
||||
"count_before": count_before,
|
||||
"count_after": stable.get("count_after"),
|
||||
"delete": delete_result,
|
||||
"upload_state": stable.get("upload_state"),
|
||||
"stable": stable,
|
||||
}
|
||||
before = stable.get("rects") or _image_rects(cdp)
|
||||
else:
|
||||
stable = _wait_image_manager_stable(
|
||||
cdp,
|
||||
expected_count=len(before),
|
||||
timeout=20,
|
||||
settle_seconds=1.0,
|
||||
require_upload_input=True,
|
||||
)
|
||||
if not stable.get("ok"):
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": stable.get("reason"),
|
||||
"count_before": count_before,
|
||||
"count_after": stable.get("count_after"),
|
||||
"upload_state": stable.get("upload_state"),
|
||||
"stable": stable,
|
||||
}
|
||||
before = stable.get("rects") or _image_rects(cdp)
|
||||
backup_path = _validated_old_cover_backup(old_cover_path)
|
||||
if not backup_path:
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": "OLD_COVER_BACKUP_MISSING",
|
||||
"count_before": count_before,
|
||||
"old_cover_path": old_cover_path,
|
||||
}
|
||||
delete_result = _delete_first_cover(cdp, before)
|
||||
if not delete_result.get("ok"):
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": delete_result.get("reason"),
|
||||
"count_before": count_before,
|
||||
"delete": delete_result,
|
||||
}
|
||||
stable = _wait_image_manager_stable(
|
||||
cdp,
|
||||
expected_count=delete_result.get("count_after"),
|
||||
timeout=30,
|
||||
settle_seconds=2.0,
|
||||
require_upload_input=True,
|
||||
)
|
||||
if not stable.get("ok"):
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": stable.get("reason"),
|
||||
"count_before": count_before,
|
||||
"count_after": stable.get("count_after"),
|
||||
"delete": delete_result,
|
||||
"upload_state": stable.get("upload_state"),
|
||||
"stable": stable,
|
||||
}
|
||||
before = stable.get("rects") or _image_rects(cdp)
|
||||
before_srcs = {r.get("src") for r in before}
|
||||
upload_click = _click_upload_tile(cdp)
|
||||
if not upload_click.get("clicked"):
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": "UPLOAD_TILE_NOT_READY",
|
||||
"count_before": count_before,
|
||||
"delete": delete_result,
|
||||
"upload_click": upload_click,
|
||||
"upload_state": _upload_state(cdp),
|
||||
}
|
||||
time.sleep(1)
|
||||
oid = cdp.object_id("document.querySelector('.shopee-image-manager__upload input[type=file]')")
|
||||
if not oid:
|
||||
return {
|
||||
@@ -788,6 +818,7 @@ def replace_cover(cdp, image_win_path, old_cover_path=None, timeout=180) -> dict
|
||||
"reason": "NO_UPLOAD_INPUT",
|
||||
"count_before": count_before,
|
||||
"delete": delete_result,
|
||||
"upload_click": upload_click,
|
||||
"upload_state": _upload_state(cdp),
|
||||
}
|
||||
|
||||
@@ -807,6 +838,7 @@ def replace_cover(cdp, image_win_path, old_cover_path=None, timeout=180) -> dict
|
||||
"reason": "UPLOAD_INPUT_NOT_READY",
|
||||
"count_before": count_before,
|
||||
"delete": delete_result,
|
||||
"upload_click": upload_click,
|
||||
"upload_state": last_state,
|
||||
}
|
||||
blob_seen = bool(last_state.get("blob_count"))
|
||||
@@ -816,6 +848,18 @@ def replace_cover(cdp, image_win_path, old_cover_path=None, timeout=180) -> dict
|
||||
cur = _image_rects(cdp)
|
||||
last_state = _upload_state(cdp)
|
||||
blob_seen = blob_seen or any(str(r.get("src") or "").startswith("blob:") for r in cur)
|
||||
if (last_state.get("errors") or last_state.get("error_toasts")) and not last_state.get("busy_count"):
|
||||
reason = "UPLOAD_DUPLICATE_IMAGE" if _has_duplicate_upload_error(last_state) else "UPLOAD_PAGE_ERROR"
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": reason,
|
||||
"count_before": count_before,
|
||||
"count_after": len(cur),
|
||||
"delete": delete_result,
|
||||
"upload_click": upload_click,
|
||||
"upload_state": last_state,
|
||||
"file_size": file_size,
|
||||
}
|
||||
ready = [
|
||||
r for r in cur
|
||||
if r.get("src") not in before_srcs
|
||||
@@ -826,16 +870,6 @@ def replace_cover(cdp, image_win_path, old_cover_path=None, timeout=180) -> dict
|
||||
if ready and (len(cur) > len(before) or len(ready) == 1):
|
||||
new_src = ready[-1]["src"]
|
||||
break
|
||||
if (last_state.get("errors") or last_state.get("error_toasts")) and not last_state.get("busy_count"):
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": "UPLOAD_PAGE_ERROR",
|
||||
"count_before": count_before,
|
||||
"count_after": len(cur),
|
||||
"delete": delete_result,
|
||||
"upload_state": last_state,
|
||||
"file_size": file_size,
|
||||
}
|
||||
if last_state.get("crop_modal"):
|
||||
return {
|
||||
"ok": False,
|
||||
@@ -843,6 +877,7 @@ def replace_cover(cdp, image_win_path, old_cover_path=None, timeout=180) -> dict
|
||||
"count_before": count_before,
|
||||
"count_after": len(cur),
|
||||
"delete": delete_result,
|
||||
"upload_click": upload_click,
|
||||
"upload_state": last_state,
|
||||
"file_size": file_size,
|
||||
}
|
||||
@@ -856,6 +891,7 @@ def replace_cover(cdp, image_win_path, old_cover_path=None, timeout=180) -> dict
|
||||
"count_before": count_before,
|
||||
"count_after": len(cur),
|
||||
"delete": delete_result,
|
||||
"upload_click": upload_click,
|
||||
"upload_state": last_state,
|
||||
"blob_seen": blob_seen,
|
||||
"file_size": file_size,
|
||||
@@ -881,6 +917,7 @@ def replace_cover(cdp, image_win_path, old_cover_path=None, timeout=180) -> dict
|
||||
"count_before": count_before,
|
||||
"count_after": len(after),
|
||||
"delete": delete_result,
|
||||
"upload_click": upload_click,
|
||||
}
|
||||
|
||||
|
||||
@@ -1069,6 +1106,7 @@ def _close_applied_product(cdp, close_success_tab=False):
|
||||
finally:
|
||||
if close_success_tab and created_by_app and target_id:
|
||||
try:
|
||||
time.sleep(2)
|
||||
close_tab(target_id, host=host)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user