Files
Misaka 3c32720985 feat(sites): auto-screenshot on final download failure for debugging
所有站点 with_retry 在最后一次重试失败、reset 之前自动截图,
保存到 logs/screenshots/。网页站点走 Playwright page.screenshot(),
安能走 CDP Page.captureScreenshot。截图失败绝不阻塞任务流程。

- paths.py: 新增 SCREENSHOT_DIR (BASE_DIR/logs/screenshots/)
- runtime.py: 新增 capture_error_screenshot() 工具函数
- .gitignore: 新增 logs/ 忽略规则

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-02 11:18:21 +08:00

292 lines
13 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# sites/baishi.py
import os
import yaml
from playwright.sync_api import sync_playwright
from inbound_verify.paths import DOWNLOAD_DIR, CONFIG_PATH
from inbound_verify import state_store
def with_retry(site_name, label, flow, reset, max_attempts=3, page=None):
"""异常兜底flow 失败 → 重置回初始态 → 重试,最多 max_attempts 次(含首次)。
每次失败都重置(含最终放弃那一次):既是重试前的清场,也保证最终放弃时环境干净。
最后一次失败时(重置前)自动截图保存到 logs/screenshots/,供问题排查。
flow 为零参可调用;返回 False 视为失败,其余视为成功。
返回 True=最终成功False=重试耗尽放弃(供调度层判断任务成败)。
"""
for attempt in range(1, max_attempts + 1):
try:
ret = flow()
if ret is False:
raise RuntimeError("流程返回失败状态")
if attempt > 1:
print(f">> 【{site_name}-{label}】第 {attempt} 次尝试成功 ✅")
return True
except Exception as e:
print(f"⚠️ 【{site_name}-{label}】第 {attempt}/{max_attempts} 次失败: {e}")
if attempt == max_attempts and page is not None:
try:
from inbound_verify.runtime import capture_error_screenshot
capture_error_screenshot(page, site_name, label, attempt, str(e))
except Exception:
pass
print(f" → 重置【{site_name}】到初始态,清理环境 ...")
try:
reset()
except Exception as re:
print(f" ⚠️ 重置异常: {re}")
if attempt < max_attempts:
continue
print(
f"❌ 【{site_name}-{label}】已达最大尝试次数 {max_attempts},放弃(环境已清理)。"
)
return False
# 站点首页 URL异常兜底重置用也供 runtime 的 SITES_CONFIG 引用(单一来源)
HOME_URL = "https://v5.800best.com"
def baishi_reset(page):
"""异常兜底:重置百世到初始态(跳首页 URL丢弃当前页面状态登录态保留"""
page.goto(HOME_URL)
page.wait_for_timeout(1500)
def dismiss_baishi_popups(page):
"""清理百世首页杂乱弹窗:优惠券广告 / 通知消息 / 配置检查面板 / 聊天通知。
所有弹窗都有时序问题——不是登录瞬间就出现,旧代码一登录就判定"无弹窗"而跳过,
导致从未成功关闭过任何一个。修复策略统一为「轮询等待出现 → 关闭」。
经 CDP 端到端验证的选择器2026-07-19
- ① 优惠券广告:全屏居中 Ant Design modal.ant-modal-wrap.ant-modal-centered
关闭键 .ant-modal-close纯图标×、无文字。登录后约 2s 才加载。
- ② Ant Design 通知("您有新的消息".ant-notification 组件,
右下角卡片,关闭键 .ant-notification-notice-close纯图标×
- ③ 配置检查面板:.modal-config-check.react-draggable可拖拽浮动面板
默认 display:none系统检测后展开为 block。底部 footer 有两个 button
"重新检查"(danger) 和 "关 闭"(primary)。点"关 闭"后面板从 DOM 移除。
- ④ 聊天/调查通知("您还有调研单未填写...".chat-notification-popover-wrapper
z-index:1000关闭键 .chat-notification-close图标×
注意:左侧首页轮播/广告横幅是页面正常内容区(无关闭键),不在此处处理。
"""
import time as _time
# 单条轮询循环:每个 tick短间隔同时检查并关闭「所有当前可见」的弹窗。
#
# 旧实现是串行四段,每段 _poll_and_click 在「未命中」时会阻塞等待整段
# max_poll_s=5s 才返回 False。于是广告段跑完(~7s)才开始处理通知、配置,
# 而广告/通知其实登录后 1~2s 就出现了,却被卡在前面段的等待窗口里,表现为
# 「检查配置 / 消息通知关得特别慢」。改为单循环后,谁先出现谁在下一个 tick
# ~350ms就被关掉四类互不排队。
#
# 选择器均经 CDP 端到端验证(见函数 docstring
targets = [
(".ant-modal-wrap.ant-modal-centered .ant-modal-close", "优惠券广告"),
(".ant-notification-notice-close", "通知消息"),
(".modal-config-check button:has-text('关 闭')", "配置检查面板"),
(".chat-notification-close", "聊天调查通知"),
]
deadline = _time.monotonic() + 12.0 # 最长处理 12s兜底正常几秒内结束
tick_ms = 350
idle_rounds = 0 # 连续无处理的轮数
while _time.monotonic() < deadline:
handled_any = False
for sel, label in targets:
try:
loc = page.locator(sel)
if loc.count() > 0 and loc.first.is_visible(timeout=120):
loc.first.click(timeout=2000)
handled_any = True
page.wait_for_timeout(150) # 留一点关闭动画时间
except Exception:
pass
if handled_any:
idle_rounds = 0
else:
idle_rounds += 1
if idle_rounds >= 3: # 连续 ~1.05s 无任何弹窗 → 提前结束
break
page.wait_for_timeout(tick_ms)
def _remove_if_exists(path):
"""删除文件(若存在):流程开头清理上次的最终文件,避免无数据/失败时残留旧数据。"""
try:
if os.path.exists(path):
os.remove(path)
except Exception:
pass
def _close_tab(page, tab_name):
"""关闭指定名称的百世标签页。
通过 li > span 中的文本定位标签,并点击其内部 title 为 "关闭标签页" 的图标。
"""
try:
tab = page.locator("li").filter(has=page.locator("span", has_text=tab_name))
if tab.count() == 0:
print(f" 未找到标签页【{tab_name}】(可能尚未打开或已关闭),跳过。")
return
# 点击百世特有的关闭按钮
tab.first.locator("i[title='关闭标签页']").click()
print(f" 🗙 已关闭标签页【{tab_name}")
page.wait_for_timeout(300)
except Exception as e:
print(f" ⚠️ 关闭标签页【{tab_name}】时出错: {e}")
def baishi_download_undelivered_data(page, force=False, date=None):
"""百世:一键提取应到未到(当日未扫)数据(内部含异常兜底重试,路由层无感)。
date 形参仅为对齐统一透传签名(百世固定下载当天),忽略。"""
return with_retry(
"百世",
"应到未到",
lambda: baishi_download_undelivered_data_impl(page),
lambda: baishi_reset(page),
page=page,
)
def baishi_download_undelivered_data_impl(page):
"""百世:一键提取应到未到(当日未扫)数据(单次执行,无重试;供自动化测试用)。"""
print("\n▶ 开始执行【百世 - 一键提取应到未到数据】任务...")
download_dir = DOWNLOAD_DIR
if not os.path.exists(download_dir):
os.makedirs(download_dir)
# 清理上次的最终文件,避免本次无数据/失败时残留旧数据误导比对
_remove_if_exists(os.path.join(download_dir, "百世-应到未到货物数据.xlsx"))
try:
# 1. 导航与页面加载
print(">> 正在进入【扫描综合查询】界面...")
# 打开基础服务菜单面板
page.locator("div.nav-level1", has_text="基础服务").click()
# 限制在菜单面板nav-level2-wrapper内查找避免命中右侧同名标签页
page.locator(".nav-level2-wrapper").locator(
"a", has_text="扫描综合查询"
).click()
# 验证表格主界面加载完毕
page.get_by_role("tab", name="实时扫描率").wait_for(state="visible")
print("✅ 扫描综合查询界面已加载")
# 1.5 顺手抓取「到/接件扫描 → 当日」的 应扫/已扫(=应到/实到基数),
# 供汇总报表填写百世行的应到件/实到件。
# 实时扫描率表头为 14 列 leaf发/交件[昨日×3, 当日×4] +
# 到/接件[昨日×3, 当日×4],到/接件当日四列索引为
# 10(应扫) 11(已扫) 12(未扫) 13(率)。(与下方 nth(12) 未扫同源)
try:
_first_row = page.locator(".ant-table-tbody > tr").first
_exp_txt = (
_first_row.locator("td").nth(10).inner_text().strip().replace(",", "")
)
_arr_txt = (
_first_row.locator("td").nth(11).inner_text().strip().replace(",", "")
)
def _to_int(v):
try:
return int(float(v)) if v not in ("", "-") else 0
except (TypeError, ValueError):
return 0
_exp_n, _arr_n = _to_int(_exp_txt), _to_int(_arr_txt)
if _exp_n > 0:
state_store.set_setting("百世", "scan_expected_pieces", str(_exp_n))
state_store.set_setting("百世", "scan_arrived_pieces", str(_arr_n))
from inbound_verify import (
store,
) # 直接落库 PG一步不绕 state_store→store
store.upsert_baishi_daily_stats(_exp_n, _arr_n)
print(f" 已记录百世应到/实到基数:应扫 {_exp_n} / 已扫 {_arr_n}")
except Exception as _e:
# 抓取失败绝不影响未到明细下载主流程
print(f" ⚠️ 抓取百世应到/实到基数失败(不影响未到明细下载):{_e}")
# 2. 精准定位并点击【到/接件扫描 -> 当日 -> 未扫】的数字控件
print(">> 正在解析表格,提取当日到件未扫明细...")
# 定位第一行数据的第 13 列(索引 12
target_cell = page.locator(".ant-table-tbody > tr").first.locator("td").nth(12)
# 特殊情况处理检查未扫数量是否为0
cell_text = target_cell.inner_text().strip()
if cell_text == "0" or cell_text == "":
print(
f" 注意:当日未扫数量为【{cell_text}】,无数据需要提取,任务结束。"
)
# 数据为空时,提前结束前也需清理环境
_close_tab(page, "扫描综合查询")
return True
# 有未扫数据,继续点击操作
target_cell.locator("a").click()
# 等待下方弹出的明细表格区域加载完毕
detail_section = page.locator(".m-query-all-scanRateDetail")
detail_section.wait_for(state="visible")
page.wait_for_timeout(1000)
# 3. 触发导出设置模态框
print(">> 正在打开导出配置面板...")
# 使用明细区域内的导出按钮,避免误点主表导出
detail_section.locator(".export-wrap a[title='导出']").click()
# 验证模态框弹出
page.locator(".ant-modal-title", has_text="导出设置").wait_for(state="visible")
# 4. 读取百世导出密码(存 state.db由前端站点配置
print(">> 正在读取配置并填写密码...")
password = state_store.get_setting("百世", "password")
if not password:
print(" ⚠️ 警告:未设置百世导出密码(前端站点配置),可能导致导出失败。")
page.get_by_placeholder("请输入登录密码").fill(password)
# 5. 执行最终下载
print(">> 正在下载...")
with page.expect_download() as download_info:
# 点击模态框底部的“导 出”按钮
page.locator(".ant-modal-footer").get_by_role(
"button", name="导 出"
).click()
download = download_info.value
save_path = os.path.join(download_dir, "百世-应到未到货物数据.xlsx")
# 如果之前已经有同名文件,覆盖保存
if os.path.exists(save_path):
os.remove(save_path)
download.save_as(save_path)
print(f"====================================================")
print(f" 提取成功。")
print(f" 已下载: {save_path}")
print(f"====================================================")
# 6. 环境清理
print(">> 任务完成,正在清理环境...")
_close_tab(page, "扫描综合查询")
print("\n【百世 - 应到未到数据提取】流程结束。")
return True
except Exception as e:
print(f"\n❌ 任务执行过程中发生异常: {e}")
return False