Files
InboundVerify/site_baishi.py
Misaka 66bd8af421 阶段1:服务化骨架(FastAPI + Playwright worker + 任务队列)
- 新增 runtime.py:抽离共享核心 launch_and_prepare / dispatch_task / run_heartbeat /
  probe_* / launch_anneng / RuntimeContext,常量 SITES_CONFIG 等;main_router 复用
- 新增 server.py:FastAPI(主线程)+ Playwright worker(独立线程)+ 任务队列;
  API:POST/GET /tasks、GET /status、GET /data/{file}(防路径穿越)
- state_store:加 task_history 表 + create/update/get/list 接口
- 5 站点 with_retry 改为返回 True/False,供 dispatch_task 判成败
- main_router:重写为复用 runtime 的交互模式(行为不变)
- requirements:加 fastapi、uvicorn
- CLAUDE.md:补充运行模式与共享核心架构说明

线程模型:主线程 FastAPI 不碰 Playwright,worker 线程独占 page,经 Queue + SQLite 通信。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-17 00:04:39 +08:00

195 lines
7.6 KiB
Python
Raw 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.
# site_baishi.py
import os
import yaml
from playwright.sync_api import sync_playwright
from paths import DOWNLOAD_DIR, CONFIG_PATH
def with_retry(site_name, label, flow, reset, max_attempts=3):
"""异常兜底flow 失败 → 重置回初始态 → 重试,最多 max_attempts 次(含首次)。
每次失败都重置(含最终放弃那一次):既是重试前的清场,也保证最终放弃时环境干净。
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}")
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异常兜底重置用也供 main_router 的 SITES_CONFIG 引用(单一来源)
HOME_URL = "https://v5.800best.com"
def baishi_reset(page):
"""异常兜底:重置百世到初始态(跳首页 URL丢弃当前页面状态登录态保留"""
page.goto(HOME_URL)
page.wait_for_timeout(1500)
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):
"""百世:一键提取应到未到(当日未扫)数据(内部含异常兜底重试,路由层无感)。"""
return with_retry(
"百世",
"应到未到",
lambda: baishi_download_undelivered_data_impl(page),
lambda: baishi_reset(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("✅ 扫描综合查询界面已加载")
# 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. 加载 YAML 配置并输入密码
print(">> 正在读取配置并填写密码...")
password = ""
try:
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
config = yaml.safe_load(f)
password = config.get("baishi", {}).get("password", "")
if not password:
print(
" ⚠️ 警告:在 config.yaml 中未找到百世的登录密码,可能导致导出失败。"
)
except Exception as e:
print(f" ⚠️ 读取 config.yaml 失败,请确保文件存在且格式正确: {e}")
return False
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