fix(editor): ignore logistics toasts during cover upload
This commit is contained in:
+43
-2
@@ -37,6 +37,27 @@ LEGACY_ITEMBOX_XPATH = (
|
||||
COVER_FOREGROUND_RECOVERY_TIMEOUT_SECONDS = 120
|
||||
COVER_FOREGROUND_UPLOAD_RECOVERY_REASONS = {"UPLOAD_STILL_PROCESSING", "UPLOAD_TIMEOUT"}
|
||||
COVER_FOREGROUND_DRAG_RECOVERY_REASONS = {"DRAG_NOT_FIRST", "NEW_IMAGE_NOT_FOUND"}
|
||||
UPLOAD_TOAST_CONTEXT_TOKENS = (
|
||||
"圖片",
|
||||
"图片",
|
||||
"封面",
|
||||
"照片",
|
||||
"相片",
|
||||
"圖像",
|
||||
"图像",
|
||||
"image",
|
||||
"photo",
|
||||
"cover",
|
||||
"upload",
|
||||
"上傳",
|
||||
"上传",
|
||||
"檔案",
|
||||
"文件",
|
||||
"file",
|
||||
"重複",
|
||||
"重复",
|
||||
"duplicate",
|
||||
)
|
||||
|
||||
LOGIN_PATH_MARKERS = (
|
||||
"/login",
|
||||
@@ -134,7 +155,7 @@ JS_UPLOAD_STATE = (
|
||||
"var upload=cmMainUploadInput(image);var uploadBox=cmMainUploadBox(image);"
|
||||
"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=/(失敗|失败|錯誤|错误|不支援|不支持|格式|大小|尺寸|超過|超过|重複|重复|duplicate|error|fail|invalid|unsupported)/i;"
|
||||
"var uploadRe=/(圖片|图片|封面|照片|相片|圖像|图像|image|photo|cover|upload|上傳|上传|檔案|文件|file|格式|大小|尺寸|像素|解析度|分辨率|超過|超过|重複|重复|duplicate)/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);"
|
||||
@@ -656,8 +677,28 @@ def _image_rects(cdp):
|
||||
return _json_value(cdp, JS_RECTS, default=[]) or []
|
||||
|
||||
|
||||
def _is_upload_error_text(value):
|
||||
text = str(value or "").strip().lower()
|
||||
return bool(text) and any(token in text for token in UPLOAD_TOAST_CONTEXT_TOKENS)
|
||||
|
||||
|
||||
def _normalize_upload_state(state):
|
||||
normalized = dict(state or {})
|
||||
candidates = []
|
||||
for key in ("page_error_toasts", "error_toasts"):
|
||||
for value in normalized.get(key) or []:
|
||||
if value not in candidates:
|
||||
candidates.append(value)
|
||||
normalized["error_toasts"] = [
|
||||
value for value in candidates if _is_upload_error_text(value)
|
||||
][:8]
|
||||
return normalized
|
||||
|
||||
|
||||
def _upload_state(cdp):
|
||||
return _json_value(cdp, JS_UPLOAD_STATE, default={}) or {}
|
||||
return _normalize_upload_state(
|
||||
_json_value(cdp, JS_UPLOAD_STATE, default={}) or {}
|
||||
)
|
||||
|
||||
|
||||
def _click_upload_tile(cdp):
|
||||
|
||||
@@ -539,7 +539,7 @@ data/images/<batch_id>/<slug>/<task_id>_<item_id>_new.<ext> # AI 生成的新
|
||||
| 主图范围与读旧封面 | 主定位为 `data-product-edit-field-unique-id="images"` 内的 `.shopee-image-manager`,其中可拖拽且 `data-draggable=true` 的第一张 itembox `img.src` 是旧封面(`susercontent` CDN);只有业务字段根不存在时才回退旧全局图片管理器/itembox 定位 |
|
||||
| 商品套图原主图读取 | 复用 `open_product(..., bring_to_front=False)` 后台只读打开商品详情页,使用已验证 itembox 顺序读取全部主图 `img.src` 并返回 `{index, src}`;不要求上传 input 之外的新选择器、不改标题/封面、不拖拽、不点击更新;URL 读取完成后再由最多2个图片下载 worker 落盘;本轮自动新建 tab 按采集规则关闭,复用用户已有 tab 不关闭 |
|
||||
| 上传输入框 | 与主图读取共用 `images` 业务字段内同一个 `.shopee-image-manager`,从中取唯一 `.shopee-image-manager__upload input[type=file]`;上传前先点击同一 manager 内 `.shopee-image-manager__upload` 上传块以模拟人工选择图片入口,短暂等待后重新获取 input,再用 `DOM.setFileInputFiles` 传 Windows 路径并派发 `input`/`change` |
|
||||
| 上传成功 | 上传前先等图片管理器稳定。注意分两种状态:未满 9 张时,上传前要求图片 src 连续稳定、无 loading/blob、上传 input 存在且未禁用;满 9 张时,删除第一张之前只要求现有图片列表稳定,不得要求上传 input 可用,因为 Shopee 可能因满格隐藏/禁用上传入口;删除成功后再要求上传 input 恢复可用。上传后等新图 src 为 `susercontent`。若手动上传成功但自动上传一直转圈,优先检查是否绕过了上传块点击导致 Shopee 前端上传队列未完整初始化;代码应走“点击上传块 → 等待 → 重新取 input → `DOM.setFileInputFiles`”的人工等价路径。T-404 补丁后超时失败会返回 `upload_state`,区分仍在转圈(`UPLOAD_STILL_PROCESSING`)、图片上传错误(`UPLOAD_PAGE_ERROR`)、裁剪弹窗(`UPLOAD_CROP_REQUIRED`)和上传入口未恢复(`UPLOAD_INPUT_NOT_READY`);上传阶段只能把图片管理器内错误或图片/文件/上传相关 toast 归为封面上传错误,物流/备货等页面级校验错误不能阻断封面上传,应留到点击「更新」提交阶段处理;`有1張重複的圖片` / `重複` / `重复` / `duplicate` 属于封面上传错误,必须立即失败并提示新封面与现有商品图片重复 |
|
||||
| 上传成功 | 上传前先等图片管理器稳定。注意分两种状态:未满 9 张时,上传前要求图片 src 连续稳定、无 loading/blob、上传 input 存在且未禁用;满 9 张时,删除第一张之前只要求现有图片列表稳定,不得要求上传 input 可用,因为 Shopee 可能因满格隐藏/禁用上传入口;删除成功后再要求上传 input 恢复可用。上传后等新图 src 为 `susercontent`。若手动上传成功但自动上传一直转圈,优先检查是否绕过了上传块点击导致 Shopee 前端上传队列未完整初始化;代码应走“点击上传块 → 等待 → 重新取 input → `DOM.setFileInputFiles`”的人工等价路径。T-404 补丁后超时失败会返回 `upload_state`,区分仍在转圈(`UPLOAD_STILL_PROCESSING`)、图片上传错误(`UPLOAD_PAGE_ERROR`)、裁剪弹窗(`UPLOAD_CROP_REQUIRED`)和上传入口未恢复(`UPLOAD_INPUT_NOT_READY`);上传阶段只能把图片管理器内错误或明确包含图片、封面、上传、文件、重复等语义的 toast 归为封面上传错误,`超過/超过`、格式、大小、尺寸等通用限定词不能单独构成上传语义;物流/备货等页面级校验错误不能阻断封面上传,应保留在 `page_error_toasts` 并留到点击「更新」提交阶段处理;`圖片大小超過限制`、`有1張重複的圖片` / `重複` / `重复` / `duplicate` 属于封面上传错误,必须立即失败并提示对应原因 |
|
||||
| 封面=第一位 | `Input.dispatchMouseEvent` 拖到第一位,落点 `第一张.left - 0.30*宽` |
|
||||
| 换封面删除 | 更新封面统一先删当前第一张,不再只限满 9 张;先确认本地旧封面备份存在,再点第一张删除(`.shopee-image-manager__icon--delete` 或同类 delete 标记)并在可见 dialog/modal/popover 内点删除/确认按钮。关键顺序:删除前只等当前图片列表稳定,不检查上传 input;删除后不能只看数量减少,必须等图片管理器达到删除后数量、无 busy/blob、上传 input 恢复并短暂稳定,再重新获取 input 上传新图、确认取得 Shopee CDN 地址后拖到第一位;备份缺失则拒绝删除。已在 9 图测试商品 `29671243750` 上实测不提交流程,8 图商品也按同一替换语义删除第一张后再上传 |
|
||||
| 更新按钮 | 页面主更新按钮为 `button.eds-button` 中 `<span>更新</span>`;③ 批量确认后逐条点提交;禁用态(校验未过)记为失败。2026-06-29 实测点击后会弹 Shopee 站点侧确认框:可见 `.eds-modal__content` / `.eds-modal__box`,标题 `確定您要更新商品嗎?`,footer 中 `立即優化` 为次按钮,`更新` 为 `eds-button--primary` 主按钮;代码必须点击确认框内主按钮 `更新` 才算提交,不点 `立即優化`。2026-06-30 实测确认成功后会跳回 `https://seller.shopee.tw/portal/product/list/all?operationSortBy=modified_time` 商品列表页,代码需记录 `post_update.url/redirected_to_list` 作为提交后观测结果;若随后要关闭本轮自动新开 tab,必须先暂停 2 秒再关闭。判断优先级:跳转到 `/portal/product/list/` 是强成功信号,应优先于残留/短暂 error toast;只有在未跳转列表页、无成功 toast,且错误 toast 持续存在时,才判 `POST_UPDATE_ERROR`。未处理确认框时不得认为已提交 |
|
||||
|
||||
+12
-8
@@ -3,7 +3,7 @@ id: T-702
|
||||
title: 修复物流校验 Toast 被误判为封面上传失败
|
||||
phase: 8
|
||||
deps: [T-571]
|
||||
status: TODO
|
||||
status: DONE
|
||||
created: 2026-07-27
|
||||
---
|
||||
|
||||
@@ -56,13 +56,13 @@ created: 2026-07-27
|
||||
|
||||
## 验收要点
|
||||
|
||||
- [ ] `超過物流最長邊限制` 和 `此物流選項不支援較長備貨商品` 只保留在 `page_error_toasts`,不进入 `error_toasts`。
|
||||
- [ ] 页面存在上述物流提示时,删除后的图片区域仍能进入稳定状态,新封面可上传并拖到第一位。
|
||||
- [ ] `圖片大小超過限制`、`圖片格式錯誤` 和 `有1張重複的圖片` 仍会中止封面上传并给出中文错误。
|
||||
- [ ] 新封面成功标准仍是不在上传前快照中的新 `susercontent` 地址,不把数量变化或任意 Toast 当作成功。
|
||||
- [ ] 最终提交遇到真实物流校验时仍能报告失败,不自动修改或绕过物流配置。
|
||||
- [ ] 不修改标题更新、旧封面备份校验、删除第一张、上传入口点击、拖拽排序和提交确认逻辑。
|
||||
- [ ] 验证通过:
|
||||
- [x] `超過物流最長邊限制` 和 `此物流選項不支援較長備貨商品` 只保留在 `page_error_toasts`,不进入 `error_toasts`。
|
||||
- [x] 页面存在上述物流提示时,删除后的图片区域仍能进入稳定状态,新封面可上传并拖到第一位。
|
||||
- [x] `圖片大小超過限制`、`圖片格式錯誤` 和 `有1張重複的圖片` 仍会中止封面上传并给出中文错误。
|
||||
- [x] 新封面成功标准仍是不在上传前快照中的新 `susercontent` 地址,不把数量变化或任意 Toast 当作成功。
|
||||
- [x] 最终提交遇到真实物流校验时仍能报告失败,不自动修改或绕过物流配置。
|
||||
- [x] 不修改标题更新、旧封面备份校验、删除第一张、上传入口点击、拖拽排序和提交确认逻辑。
|
||||
- [x] 验证通过:
|
||||
- `py -3.10 -m unittest discover -s tests -p test_editor_login.py`
|
||||
- `py -3.10 -m unittest discover -s tests`
|
||||
- `py -3.10 -m ruff check app tests main.py`
|
||||
@@ -79,3 +79,7 @@ created: 2026-07-27
|
||||
## 执行记录
|
||||
|
||||
- 2026-07-27:根据商品 `57064892919` 的真实更新日志创建任务。确认第一次运行新封面已取得蝦皮 CDN 地址,但 `超過物流最長邊限制` 因通用关键词 `超過` 被误归类为上传错误;第二次运行又在删除后稳定检查阶段被同一物流提示阻断。
|
||||
- 2026-07-27:收紧 `JS_UPLOAD_STATE` 的上传语义关键词,移除不能单独证明上传错误的格式、大小、尺寸、像素、分辨率和超过等通用词;新增 Python 侧 `_normalize_upload_state()` 作为最终分类边界,物流提示继续保留在 `page_error_toasts`,但不再进入阻断上传的 `error_toasts`。
|
||||
- 2026-07-27:扩展 `FakeCoverCDP` 回归场景,覆盖真实文案 `超過物流最長邊限制`、较长备货物流提示、图片大小超限和重复图片;确认物流提示不阻断删除后稳定、上传和拖拽,明确图片错误仍会阻断。
|
||||
- 2026-07-27:同步 `docs/04-architecture.md` 第七节。未改变旧封面备份、删除第一张、上传入口点击、CDN 成功判据、拖拽、前台恢复或最终提交逻辑。
|
||||
- 2026-07-27:验证通过:`py -3.10 -m unittest discover -s tests -p test_editor_login.py`(65 项)、`py -3.10 -m unittest discover -s tests`(700 项)、`py -3.10 -m ruff check app tests main.py`、`py -3.10 -m compileall app main.py`、`git diff --check`。未执行真实商品更新,以免在未经过③批量确认边界的情况下再次删除线上第一张封面。
|
||||
|
||||
@@ -1521,7 +1521,10 @@ class EditorLoginTests(unittest.TestCase):
|
||||
def test_replace_cover_ignores_unrelated_logistics_page_error_toast(self):
|
||||
cdp = FakeCoverCDP(
|
||||
count=9,
|
||||
page_error_toasts=["此物流選項不支援較長備貨商品"],
|
||||
page_error_toasts=[
|
||||
"超過物流最長邊限制",
|
||||
"此物流選項不支援較長備貨商品",
|
||||
],
|
||||
)
|
||||
|
||||
with mock.patch("app.editor.time.sleep"):
|
||||
@@ -1537,6 +1540,32 @@ class EditorLoginTests(unittest.TestCase):
|
||||
self.assertTrue(cdp.uploaded)
|
||||
self.assertTrue(cdp.dragged)
|
||||
|
||||
def test_upload_state_requires_explicit_image_context_for_page_error_toast(self):
|
||||
cdp = FakeCoverCDP(
|
||||
page_error_toasts=[
|
||||
"超過物流最長邊限制",
|
||||
"此物流選項不支援較長備貨商品",
|
||||
"圖片大小超過限制",
|
||||
"有1張重複的圖片",
|
||||
],
|
||||
)
|
||||
|
||||
state = editor._upload_state(cdp)
|
||||
|
||||
self.assertEqual(
|
||||
[
|
||||
"超過物流最長邊限制",
|
||||
"此物流選項不支援較長備貨商品",
|
||||
"圖片大小超過限制",
|
||||
"有1張重複的圖片",
|
||||
],
|
||||
state["page_error_toasts"],
|
||||
)
|
||||
self.assertEqual(
|
||||
["圖片大小超過限制", "有1張重複的圖片"],
|
||||
state["error_toasts"],
|
||||
)
|
||||
|
||||
|
||||
def test_replace_cover_waits_for_deleted_images_to_settle_before_uploading(self):
|
||||
cdp = FakeCoverCDP(count=9, post_delete_busy_polls=2)
|
||||
|
||||
Reference in New Issue
Block a user