fix: avoid focusing chrome during collection
This commit is contained in:
+10
-2
@@ -160,12 +160,20 @@ def find_product_tab(item_id, host=None):
|
||||
return None
|
||||
|
||||
|
||||
def create_tab_info(url, host=None):
|
||||
def create_tab_info(url, host=None, background=False):
|
||||
"""用 browser 级 Target.createTarget 新建 tab,返回其 page target 信息。"""
|
||||
ver = http_get("/json/version", host=host)
|
||||
b = CDP(ver["webSocketDebuggerUrl"])
|
||||
try:
|
||||
tid = b.send("Target.createTarget", {"url": url})["targetId"]
|
||||
params = {"url": url}
|
||||
if background:
|
||||
params["background"] = True
|
||||
try:
|
||||
tid = b.send("Target.createTarget", params)["targetId"]
|
||||
except RuntimeError:
|
||||
if not background:
|
||||
raise
|
||||
tid = b.send("Target.createTarget", {"url": url})["targetId"]
|
||||
finally:
|
||||
b.close()
|
||||
end = time.time() + 15
|
||||
|
||||
+8
-7
@@ -753,7 +753,7 @@ def _close_open_product_failure(cdp):
|
||||
pass
|
||||
|
||||
|
||||
def open_product(account, item_id, on_step=None) -> CDP:
|
||||
def open_product(account, item_id, on_step=None, bring_to_front=True) -> CDP:
|
||||
"""Open or reuse a product edit tab, navigate to a clean edit URL, and wait ready."""
|
||||
|
||||
_notify_collect_step(on_step, "open_product")
|
||||
@@ -763,7 +763,7 @@ def open_product(account, item_id, on_step=None) -> CDP:
|
||||
tab = find_product_tab(item_id, host=host)
|
||||
created_by_app = tab is None
|
||||
if tab is None:
|
||||
tab = create_tab_info(url, host=host)
|
||||
tab = create_tab_info(url, host=host, background=not bring_to_front)
|
||||
ws = tab["webSocketDebuggerUrl"]
|
||||
cdp = CDP(ws)
|
||||
cdp.target_id = tab.get("id")
|
||||
@@ -772,10 +772,11 @@ def open_product(account, item_id, on_step=None) -> CDP:
|
||||
try:
|
||||
_ensure_page_domains(cdp)
|
||||
install_toast_observer(cdp)
|
||||
try:
|
||||
cdp.send("Page.bringToFront")
|
||||
except Exception:
|
||||
pass
|
||||
if bring_to_front:
|
||||
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")
|
||||
@@ -826,7 +827,7 @@ def collect(account, task, on_step=None) -> dict:
|
||||
"""Collect current title and cover snapshot before any edits."""
|
||||
|
||||
item_id = _item_id(task)
|
||||
cdp = open_product(account, item_id, on_step=on_step)
|
||||
cdp = open_product(account, item_id, on_step=on_step, bring_to_front=False)
|
||||
try:
|
||||
_notify_collect_step(on_step, "read_title")
|
||||
old_title = read_title(cdp)
|
||||
|
||||
@@ -389,6 +389,7 @@ data/images/<batch_id>/<slug>/<task_id>_<item_id>_new.<ext> # AI 生成的新
|
||||
|
||||
- 用账号 Chrome 打开商品页,等就绪,读旧标题(标题输入框 value)。
|
||||
- `open_product` 先复用已打开的同商品 tab;没有才新建商品编辑页 tab。采集完成后只关闭本次程序自动新建的商品 tab,不关闭用户原本已经打开的 tab。`CDP.close()` 只断开 WebSocket 控制连接,不等于关闭浏览器 tab。
|
||||
- ①采集调用 `open_product(..., bring_to_front=False)`,不主动执行 `Page.bringToFront`;新建商品 tab 时尝试 `Target.createTarget(background=true)` 降低 Chrome 抢焦点概率,若当前 Chrome/CDP 不接受该参数则退回普通新建 tab。③更新仍使用默认 `bring_to_front=True`,保持可观察的上传、拖拽和提交行为。
|
||||
- 若商品 ID 失效、无权限或店铺不匹配导致商品编辑页无法就绪,`open_product` 必须读取/捕获 Shopee toast,把最近错误文案写入采集失败原因和诊断日志,不能只返回泛化超时。①列表只在明确捕获商品失效类 toast 时把“阶段”显示为“商品失效”;底层 `stage` 不新增中文值。若这个失败发生在程序自动新建的商品 tab 内,`open_product` 要关闭该 tab;复用用户已有 tab 不关闭。
|
||||
|
||||
- 旧封面:取第一张 itembox 的 `img.src`(CDN 链接),下载到 `data/images/<batch_id>/<slug>/<task_id>_<item_id>_old.jpg`。
|
||||
@@ -452,6 +453,7 @@ data/images/<batch_id>/<slug>/<task_id>_<item_id>_new.<ext> # AI 生成的新
|
||||
| 代理干扰 | 清除 `*_proxy`(requests `trust_env=False`),否则连本地 CDP 超时 |
|
||||
| WebSocket Origin | `websocket-client` `suppress_origin=True` |
|
||||
| 关闭连接 vs 关闭 tab | `CDP.close()` 只关闭 WebSocket;需要关闭浏览器页面时必须调用浏览器 target 关闭接口。采集只关闭本轮自动新建的商品页,复用的用户已有 tab 不关闭;③ 仅在设置 `close_success_tab=true`、成功提交、且 tab 为本轮自动新建时关闭;确认成功跳回商品列表页时,关闭前等待 2 秒 |
|
||||
| 采集前台激活 | ①采集只读打开商品页时不主动 `Page.bringToFront`;新建 tab 尝试 `Target.createTarget(background=true)`,不支持时退回普通新建。③更新仍保持默认前台激活 |
|
||||
| SPA 就绪 | 不用 load 事件;轮询“标题输入框 + 图片 itembox + 上传输入框”三者都在 |
|
||||
| 商品页错误 toast | Shopee 错误提示使用 `.eds-toasts` / `.eds-toast__content`,可能很快隐藏或 `display:none`。打开商品页/等待 SPA 就绪前应注入 `MutationObserver` 或等价监听,把 toast 文本、`outerHTML`、当前 URL、时间、可见状态保存到页面缓存(如 `window.__cmshopee_toasts`);等待详情页关键元素超时时,再兜底读取当前 DOM 中的 toast。最近错误 toast 应优先成为 `open_product` 失败原因,并写入 DB 运行日志和本地脱敏诊断日志。只有明确商品失效/不存在/无权限类 toast 才驱动①阶段列显示“商品失效”;网络、CDP、未登录、页面超时、风控等其他失败仍显示“失败” |
|
||||
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@
|
||||
|
||||
- 导入:openpyxl 解析**输入列**(账号名/别名/商品id)入 SQLite。
|
||||
- **导入汇总栏**(导入后即时刷新,跑采集前的校验关口):显示 文件数、解析行数(原始数据量)、有效/无效行、匹配账号行数(按账号细分)、未匹配行数。未匹配/无效数字标红可点,点击在列表筛出便于定位纠错。
|
||||
- 采集:点击后先为本轮匹配账号确保 Chrome 就绪(已开复用、未开启动),再检测登录;未登录账号的任务整组略过并汇总提示。登录账号用对应 Chrome 只读打开商品页,读旧标题、下载旧封面到 `data/images/<batch_id>/<slug>/<task_id>_<item_id>_old.jpg`,写 `old_title/old_cover_path`,stage=collected。若程序为采集自动新建商品页 tab,采集结束后自动关闭;若复用用户原本打开的 tab,则不关闭。采集结束不关闭账号 Chrome,用户可自行关闭。
|
||||
- 采集:点击后先为本轮匹配账号确保 Chrome 就绪(已开复用、未开启动),再检测登录;未登录账号的任务整组略过并汇总提示。登录账号用对应 Chrome 只读打开商品页,读旧标题、下载旧封面到 `data/images/<batch_id>/<slug>/<task_id>_<item_id>_old.jpg`,写 `old_title/old_cover_path`,stage=collected。采集不主动把商品页切到前台;程序自动新建商品页 tab 时尽量后台创建,采集结束后自动关闭;若复用用户原本打开的 tab,则不关闭。采集结束不关闭账号 Chrome,用户可自行关闭。
|
||||
- 若商品 ID 已失效、无权限或店铺不匹配,Shopee 可能只弹出短暂错误 toast;采集失败时界面日志应显示捕获到的 toast 文案,并把 toast HTML/URL 写入本地诊断日志,避免用户手动抢复制。只有明确捕获商品失效/商品不存在/无权限类 toast 时,①列表“阶段”列显示“商品失效”;其他商品页打开失败仍显示“失败”。如果失败发生在 `open_product()` 内部,本轮自动新建的商品 tab 必须关闭,复用用户已有 tab 不关闭。
|
||||
|
||||
- 回写:采集完成后自动把旧标题/旧封面路径批量回写原 Excel;保留「回写旧数据到 Excel」作为手动重试入口(原文件被锁→提示关闭后重试/另存)。
|
||||
|
||||
+5
-3
@@ -3,7 +3,7 @@ id: T-562
|
||||
title: ①采集打开商品页不主动切换 Chrome 前台
|
||||
phase: 2
|
||||
deps: [T-203, T-205b, T-560]
|
||||
status: TODO
|
||||
status: DONE
|
||||
created: 2026-07-08
|
||||
---
|
||||
|
||||
@@ -29,11 +29,11 @@ created: 2026-07-08
|
||||
- `cdp.create_tab_info(url, host=None, background=False)` 增加 `background` 参数。
|
||||
- 当 `background=True` 时,`Target.createTarget` 参数中带 `background: true`。
|
||||
- `open_product(..., bring_to_front=False)` 新建商品 tab 时传 `background=True`;复用已有 tab 时不激活。
|
||||
- 如果 Chrome 版本忽略 `background`,仍至少不再执行 `Page.bringToFront`。
|
||||
- 如果 Chrome 版本忽略或拒绝 `background`,则退回普通新建 tab,仍至少不再执行 `Page.bringToFront`。
|
||||
|
||||
3. **测试**
|
||||
- editor 单测覆盖:采集调用不发送 `Page.bringToFront`,更新调用仍发送。
|
||||
- cdp 单测覆盖:`create_tab_info(background=True)` 会把 `background: true` 传给 `Target.createTarget`。
|
||||
- cdp 单测覆盖:`create_tab_info(background=True)` 会把 `background: true` 传给 `Target.createTarget`,并覆盖 `background` 被拒绝后的普通新建回退。
|
||||
- 保持失败时自动关闭本轮新建 tab、复用用户已有 tab 不关闭的原语义。
|
||||
|
||||
## 验收要点
|
||||
@@ -50,3 +50,5 @@ created: 2026-07-08
|
||||
|
||||
## 执行记录
|
||||
|
||||
- 2026-07-08:完成。`editor.open_product()` 增加 `bring_to_front` 参数;①采集调用 `bring_to_front=False`,不主动 `Page.bringToFront`;③更新继续默认前台激活。`cdp.create_tab_info()` 增加 `background` 参数,后台创建失败时自动回退普通新建。已同步 `docs/04-architecture.md` 和 `docs/routes.md`。
|
||||
- 2026-07-08:验证通过:`python -m ruff check app tests main.py`、`py -3.10 -m compileall app main.py`、`py -3.10 -m unittest discover -s tests`、`git diff --check`。
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
|
||||
from _helpers import REPO_ROOT
|
||||
|
||||
if str(REPO_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
from app import cdp
|
||||
|
||||
|
||||
class FakeBrowserCDP:
|
||||
sent = []
|
||||
fail_first_create = False
|
||||
|
||||
def __init__(self, ws_url):
|
||||
self.ws_url = ws_url
|
||||
self.closed = False
|
||||
|
||||
def send(self, method, params=None):
|
||||
self.sent.append((method, params or {}))
|
||||
if method == "Target.createTarget":
|
||||
if self.fail_first_create:
|
||||
type(self).fail_first_create = False
|
||||
raise RuntimeError("Target.createTarget: Invalid parameters")
|
||||
return {"targetId": "target-new"}
|
||||
return {}
|
||||
|
||||
def close(self):
|
||||
self.closed = True
|
||||
|
||||
|
||||
class CdpTests(unittest.TestCase):
|
||||
def test_create_tab_info_can_request_background_target(self):
|
||||
FakeBrowserCDP.sent = []
|
||||
FakeBrowserCDP.fail_first_create = False
|
||||
responses = [
|
||||
{"webSocketDebuggerUrl": "ws-browser"},
|
||||
[
|
||||
{
|
||||
"id": "target-new",
|
||||
"type": "page",
|
||||
"url": "https://seller.shopee.tw/portal/product/1",
|
||||
"webSocketDebuggerUrl": "ws-page",
|
||||
}
|
||||
],
|
||||
]
|
||||
|
||||
with mock.patch("app.cdp.http_get", side_effect=responses), \
|
||||
mock.patch("app.cdp.CDP", FakeBrowserCDP), \
|
||||
mock.patch("app.cdp.time.sleep"):
|
||||
tab = cdp.create_tab_info(
|
||||
"https://seller.shopee.tw/portal/product/1",
|
||||
host="127.0.0.1:9222",
|
||||
background=True,
|
||||
)
|
||||
|
||||
self.assertEqual("ws-page", tab["webSocketDebuggerUrl"])
|
||||
self.assertEqual(
|
||||
[
|
||||
(
|
||||
"Target.createTarget",
|
||||
{
|
||||
"url": "https://seller.shopee.tw/portal/product/1",
|
||||
"background": True,
|
||||
},
|
||||
)
|
||||
],
|
||||
FakeBrowserCDP.sent,
|
||||
)
|
||||
|
||||
def test_create_tab_info_keeps_foreground_default_params(self):
|
||||
FakeBrowserCDP.sent = []
|
||||
FakeBrowserCDP.fail_first_create = False
|
||||
responses = [
|
||||
{"webSocketDebuggerUrl": "ws-browser"},
|
||||
[
|
||||
{
|
||||
"id": "target-new",
|
||||
"type": "page",
|
||||
"url": "https://seller.shopee.tw/portal/product/1",
|
||||
"webSocketDebuggerUrl": "ws-page",
|
||||
}
|
||||
],
|
||||
]
|
||||
|
||||
with mock.patch("app.cdp.http_get", side_effect=responses), \
|
||||
mock.patch("app.cdp.CDP", FakeBrowserCDP), \
|
||||
mock.patch("app.cdp.time.sleep"):
|
||||
cdp.create_tab_info(
|
||||
"https://seller.shopee.tw/portal/product/1",
|
||||
host="127.0.0.1:9222",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
[
|
||||
(
|
||||
"Target.createTarget",
|
||||
{"url": "https://seller.shopee.tw/portal/product/1"},
|
||||
)
|
||||
],
|
||||
FakeBrowserCDP.sent,
|
||||
)
|
||||
|
||||
def test_create_tab_info_falls_back_when_background_is_rejected(self):
|
||||
FakeBrowserCDP.sent = []
|
||||
FakeBrowserCDP.fail_first_create = True
|
||||
responses = [
|
||||
{"webSocketDebuggerUrl": "ws-browser"},
|
||||
[
|
||||
{
|
||||
"id": "target-new",
|
||||
"type": "page",
|
||||
"url": "https://seller.shopee.tw/portal/product/1",
|
||||
"webSocketDebuggerUrl": "ws-page",
|
||||
}
|
||||
],
|
||||
]
|
||||
|
||||
with mock.patch("app.cdp.http_get", side_effect=responses), \
|
||||
mock.patch("app.cdp.CDP", FakeBrowserCDP), \
|
||||
mock.patch("app.cdp.time.sleep"):
|
||||
cdp.create_tab_info(
|
||||
"https://seller.shopee.tw/portal/product/1",
|
||||
host="127.0.0.1:9222",
|
||||
background=True,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
[
|
||||
(
|
||||
"Target.createTarget",
|
||||
{
|
||||
"url": "https://seller.shopee.tw/portal/product/1",
|
||||
"background": True,
|
||||
},
|
||||
),
|
||||
(
|
||||
"Target.createTarget",
|
||||
{"url": "https://seller.shopee.tw/portal/product/1"},
|
||||
),
|
||||
],
|
||||
FakeBrowserCDP.sent,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -442,6 +442,7 @@ class EditorLoginTests(unittest.TestCase):
|
||||
self.assertEqual("127.0.0.1:9222", cdp.cdp_host)
|
||||
find_product_tab.assert_called_once_with("51100639510", host="127.0.0.1:9222")
|
||||
create_tab_info.assert_not_called()
|
||||
self.assertIn(("Page.bringToFront", {}), fake.sent)
|
||||
self.assertIn(
|
||||
(
|
||||
"Page.navigate",
|
||||
@@ -469,7 +470,52 @@ class EditorLoginTests(unittest.TestCase):
|
||||
self.assertEqual("target-new", cdp.target_id)
|
||||
self.assertTrue(cdp.created_by_app)
|
||||
self.assertEqual("127.0.0.1:9223", cdp.cdp_host)
|
||||
create_tab_info.assert_called_once()
|
||||
create_tab_info.assert_called_once_with(
|
||||
(
|
||||
"https://seller.shopee.tw/portal/product/51100639511"
|
||||
"?pageEntry=product_list&ignore-html-cache=1"
|
||||
),
|
||||
host="127.0.0.1:9223",
|
||||
background=False,
|
||||
)
|
||||
|
||||
def test_open_product_can_skip_bring_to_front_for_collection(self):
|
||||
fake = FakeProductCDP("ws-new")
|
||||
with mock.patch("app.editor.find_product_tab", return_value=None), mock.patch(
|
||||
"app.editor.create_tab_info",
|
||||
return_value={"id": "target-new", "webSocketDebuggerUrl": "ws-new"},
|
||||
) as create_tab_info, mock.patch(
|
||||
"app.editor.CDP",
|
||||
return_value=fake,
|
||||
), mock.patch("app.editor._ensure_page_domains"), mock.patch("app.editor._wait_ready"):
|
||||
cdp = editor.open_product(
|
||||
{"debug_port": 9223},
|
||||
"51100639511",
|
||||
bring_to_front=False,
|
||||
)
|
||||
|
||||
self.assertIs(fake, cdp)
|
||||
self.assertNotIn(("Page.bringToFront", {}), fake.sent)
|
||||
self.assertIn(
|
||||
(
|
||||
"Page.navigate",
|
||||
{
|
||||
"url": (
|
||||
"https://seller.shopee.tw/portal/product/51100639511"
|
||||
"?pageEntry=product_list&ignore-html-cache=1"
|
||||
)
|
||||
},
|
||||
),
|
||||
fake.sent,
|
||||
)
|
||||
create_tab_info.assert_called_once_with(
|
||||
(
|
||||
"https://seller.shopee.tw/portal/product/51100639511"
|
||||
"?pageEntry=product_list&ignore-html-cache=1"
|
||||
),
|
||||
host="127.0.0.1:9223",
|
||||
background=True,
|
||||
)
|
||||
|
||||
def test_open_product_installs_toast_observer(self):
|
||||
fake = FakeProductCDP("ws-new", ready=True)
|
||||
@@ -560,7 +606,7 @@ class EditorLoginTests(unittest.TestCase):
|
||||
cdp.created_by_app = True
|
||||
cdp.cdp_host = "127.0.0.1:9222"
|
||||
|
||||
with mock.patch("app.editor.open_product", return_value=cdp), mock.patch(
|
||||
with mock.patch("app.editor.open_product", return_value=cdp) as open_product, mock.patch(
|
||||
"app.editor.read_title",
|
||||
return_value="旧标题",
|
||||
), mock.patch(
|
||||
@@ -576,6 +622,12 @@ class EditorLoginTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual("旧标题", result["old_title"])
|
||||
open_product.assert_called_once_with(
|
||||
{"debug_port": 9222},
|
||||
"51100639510",
|
||||
on_step=None,
|
||||
bring_to_front=False,
|
||||
)
|
||||
self.assertTrue(cdp.closed)
|
||||
close_tab.assert_called_once_with("target-new", host="127.0.0.1:9222")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user