Add per-site exception fallback (reset + retry), router stays pure dispatch

每个站点模块自带"失败→重置回初始态→重试"兜底(最多 3 次,含首次;每次
失败都重置,含最终放弃那次清场),对外只暴露 xxx_download 公开入口,路由
层无感、只负责调度。

- 各站点模块新增:HOME_URL、xxx_reset、内联 with_retry;原流程拆为
  xxx_download(带重试) + xxx_download_impl(单次,供自动化测试探测原始失败)。
- 网页站重置=goto 首页 URL;安能(Electron)重置=关业务 tab+收菜单(实测
  reload 会留下杀不掉的僵尸 webContents,故不用 reload)。
- main_router:SITES_CONFIG 改引用各模块 HOME_URL(单一来源);菜单分派回归
  朴素调用;run_automation_test 改走 _impl 以保留"暴露问题"的测试本意。

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Misaka
2026-07-08 22:39:38 +08:00
parent 3ae94bef0d
commit 2d49c3d6f1
6 changed files with 371 additions and 37 deletions

View File

@@ -41,6 +41,35 @@ sys.stdout.reconfigure(encoding="utf-8")
from paths import DOWNLOAD_DIR, CONFIG_PATH
def with_retry(site_name, label, flow, reset, max_attempts=3):
"""异常兜底flow 失败 → 重置回初始态 → 重试,最多 max_attempts 次(含首次)。
每次失败都重置(含最终放弃那一次):既是重试前的清场,也保证最终放弃时环境干净。
flow 为零参可调用;返回 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
except Exception as e:
print(f"⚠️ 【{site_name}-{label}】第 {attempt}/{max_attempts} 次失败: {e}")
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},放弃(环境已清理)。"
)
CDP_PORT = 9222 # 默认端口(独立运行 site_anneng.py 时用main_router 启动时会用 set_cdp_port 覆盖
POLL_INTERVAL = 0.5
DEFAULT_TIMEOUT = 25.0
@@ -799,7 +828,13 @@ def _load_query_days():
def anneng_expected_download():
"""安能:应到货物数据(运单信息)下载,完整流程"""
"""安能:应到货物数据下载(内部含异常兜底重试,路由层无感)"""
return with_retry("安能", "应到", anneng_expected_download_impl, anneng_reset)
def anneng_expected_download_impl():
"""安能:应到货物数据(运单信息)下载,完整流程(单次执行,无重试;供自动化测试用)。"""
print("\n▶ 开始执行【安能 - 应到货物数据下载】任务 ...")
download_dir = DOWNLOAD_DIR
os.makedirs(download_dir, exist_ok=True)
@@ -1094,7 +1129,13 @@ def _save_actual(rows, download_dir):
def anneng_actual_download():
"""安能:实到数据(网点到件扫描,子单)下载,完整流程"""
"""安能:实到数据下载(内部含异常兜底重试,路由层无感)"""
return with_retry("安能", "实到", anneng_actual_download_impl, anneng_reset)
def anneng_actual_download_impl():
"""安能:实到数据(网点到件扫描,子单)下载,完整流程(单次执行,无重试;供自动化测试用)。"""
print("\n▶ 开始执行【安能 - 实到货物数据下载】任务 ...")
download_dir = DOWNLOAD_DIR
os.makedirs(download_dir, exist_ok=True)
@@ -1169,6 +1210,59 @@ def anneng_actual_download():
main_cdp.close()
def anneng_reset():
"""异常兜底:把安能重置回刚登录的初始态,供失败重试调用。
关掉所有业务 tab走 tab 条 X由 app 销毁对应 webContents含其上的弹窗+ 收起
展开的子菜单。不用 Page.reload——reload 会让业务 webContents 失去 app 引用、
变成无法清理的僵尸Target.closeTarget / window.close 都杀不掉)。
"""
try:
main_cdp = find_main_page_cdp()
except Exception as e:
print(f">> 安能重置失败:找不到主壳页面: {e}")
return False
try:
labels_json = main_cdp.eval(
"JSON.stringify([...document.querySelectorAll('.w-tabs-draggable-item')]"
".map(t => t.querySelector('span')?.textContent?.trim()).filter(Boolean))"
)
try:
labels = json.loads(labels_json or "[]")
except Exception:
labels = []
closed = 0
for label in labels:
if label and label != "工作台":
try:
close_tab_by_label(main_cdp, label)
closed += 1
time.sleep(0.2)
except Exception as e:
print(f" ⚠️ 关闭 tab「{label}」失败: {e}")
# 收起所有展开的子菜单(循环点掉每个 aria-expanded=true 的标题,含嵌套层级)
loops = 0
while main_cdp.eval(
"(() => {const e = document.querySelector"
"('div.rc-menu-submenu-title[aria-expanded=\"true\"]');"
"if (!e) return false; e.click(); return true;})()"
):
loops += 1
time.sleep(0.15)
if loops > 15:
break
print(f">> 安能已重置到初始态:关闭 {closed} 个业务 tab收起 {loops} 个子菜单")
return True
except Exception as e:
print(f">> 安能重置异常: {e}")
return False
finally:
try:
main_cdp.close()
except Exception:
pass
def main():
flow = sys.argv[1] if len(sys.argv) > 1 else "expected"
if flow == "actual":