阶段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>
This commit is contained in:
644
main_router.py
644
main_router.py
@@ -1,142 +1,35 @@
|
||||
# main_router.py
|
||||
#
|
||||
# 交互菜单模式入口(调试 / 人工操作)。核心 Playwright 管理、任务派发、心跳
|
||||
# 已抽到 runtime.py 共享;本文件只保留交互菜单与自动化测试。
|
||||
# 服务模式(常驻 + FastAPI 接收指令)见 server.py。
|
||||
|
||||
import os
|
||||
import queue
|
||||
import socket
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
import urllib.request
|
||||
from datetime import datetime
|
||||
|
||||
import yaml
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
from paths import DOWNLOAD_DIR, CONFIG_PATH
|
||||
from paths import CONFIG_PATH
|
||||
from runtime import (
|
||||
APP_SITES,
|
||||
HEARTBEAT_INTERVAL,
|
||||
dispatch_task,
|
||||
launch_and_prepare,
|
||||
run_heartbeat,
|
||||
)
|
||||
|
||||
# 阶段0:状态持久化(站点登录态 / 数据态,心跳刷新;重启不丢)
|
||||
import state_store
|
||||
|
||||
# 导入抽离出去的各个网点模块
|
||||
# 各站点模块(自动化测试 + 比对用;任务派发在 runtime)
|
||||
import site_shunxin
|
||||
import site_baishi
|
||||
import site_zto
|
||||
import site_yunda
|
||||
import site_anneng
|
||||
|
||||
# 应到未到比对(全站点,离线处理 downloads/ 下的应到/实到数据)
|
||||
import expected_undelivered
|
||||
|
||||
# 各网页站点首页 URL 统一取自对应站点模块的 HOME_URL(单一来源,不在两处维护)。
|
||||
SITES_CONFIG = {
|
||||
"顺心": site_shunxin.HOME_URL,
|
||||
"百世": site_baishi.HOME_URL,
|
||||
"中通": site_zto.HOME_URL,
|
||||
"韵达": site_yunda.HOME_URL,
|
||||
}
|
||||
|
||||
# ====================================================================
|
||||
# 站点就绪特征:每个站点登录成功进入工作台后的标志性控件
|
||||
# ====================================================================
|
||||
READY_SELECTORS = {
|
||||
"顺心": 'h1:has-text("盟商门户网")',
|
||||
"百世": 'h1[title="百世快运"]',
|
||||
"中通": '.logo:has-text("网点版")',
|
||||
"韵达": '.el-menu-item:has-text("首页")',
|
||||
}
|
||||
|
||||
# 安能是 Electron 桌面应用(不是 Playwright 打开的网页),需要单独启动。
|
||||
APP_SITES = {"安能"}
|
||||
|
||||
# 阶段0:心跳间隔(秒)——各站就绪后,每隔此时长探测一次登录态/数据态
|
||||
HEARTBEAT_INTERVAL = 30
|
||||
|
||||
# 各站最终数据文件名(用于探测"数据是否已跑出来");百世为单流程,仅应到未到
|
||||
DATA_FILENAMES = {
|
||||
"顺心": {"expected": "顺心-应到货物数据.xlsx", "actual": "顺心-实到货物数据.xlsx"},
|
||||
"中通": {"expected": "中通-应到货物数据.xlsx", "actual": "中通-实到货物数据.xlsx"},
|
||||
"韵达": {"expected": "韵达-应到货物数据.xlsx", "actual": "韵达-实到货物数据.xlsx"},
|
||||
"安能": {"expected": "安能-应到货物数据.xlsx", "actual": "安能-实到货物数据.xlsx"},
|
||||
"百世": {"expected": "百世-应到未到货物数据.xlsx", "actual": ""},
|
||||
}
|
||||
|
||||
|
||||
def probe_site_login(site_name, pages_map):
|
||||
"""探测单站是否登录(复用就绪轮询判据)。任何异常一律返回 False。
|
||||
|
||||
只在主线程调用(Playwright sync 对象绑定主线程)。
|
||||
"""
|
||||
try:
|
||||
if site_name not in pages_map:
|
||||
return False
|
||||
if site_name == "安能":
|
||||
return site_anneng.anneng_ready()
|
||||
if site_name == "顺心":
|
||||
return all(
|
||||
pg.locator(READY_SELECTORS["顺心"]).is_visible(timeout=500)
|
||||
for pg in pages_map["顺心"]
|
||||
)
|
||||
return (
|
||||
pages_map[site_name]
|
||||
.locator(READY_SELECTORS[site_name])
|
||||
.is_visible(timeout=500)
|
||||
)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def probe_data_file(site_name, kind):
|
||||
"""探测单站应到/实到数据文件是否存在且为今天。返回 (is_today: bool, mtime_str)。"""
|
||||
fname = DATA_FILENAMES.get(site_name, {}).get(kind, "")
|
||||
if not fname:
|
||||
return (False, "")
|
||||
path = os.path.join(DOWNLOAD_DIR, fname)
|
||||
if not os.path.exists(path):
|
||||
return (False, "")
|
||||
dt = datetime.fromtimestamp(os.path.getmtime(path))
|
||||
is_today = dt.date() == datetime.now().date()
|
||||
return (is_today, dt.strftime("%Y-%m-%d %H:%M:%S"))
|
||||
|
||||
|
||||
def _find_free_port():
|
||||
"""让操作系统分配一个空闲端口,避免固定端口冲突。"""
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.bind(("127.0.0.1", 0))
|
||||
return s.getsockname()[1]
|
||||
|
||||
|
||||
def _wait_cdp_up(port, timeout=60.0):
|
||||
"""轮询直到 CDP 调试端口就绪(应用启动需要时间)。"""
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
with urllib.request.urlopen(
|
||||
f"http://localhost:{port}/json/version", timeout=2
|
||||
) as resp:
|
||||
if resp.status == 200:
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(1)
|
||||
return False
|
||||
|
||||
|
||||
def launch_anneng(app_path):
|
||||
"""以调试模式启动安能 Electron 应用(自动选取空闲端口),返回子进程对象。
|
||||
|
||||
启动后请在应用内手动登录;就绪状态由 run_multi_site_daemon 的就绪轮询判断。
|
||||
"""
|
||||
port = _find_free_port()
|
||||
print(f">> 以调试模式启动【安能】应用(端口 {port}):{app_path}")
|
||||
proc = subprocess.Popen([app_path, f"--remote-debugging-port={port}"])
|
||||
site_anneng.set_cdp_port(port)
|
||||
if not _wait_cdp_up(port):
|
||||
raise RuntimeError(
|
||||
f"安能应用调试端口 {port} 未就绪——可能应用已在运行(单实例),"
|
||||
"请先关闭已有的安能窗口再试"
|
||||
)
|
||||
return proc
|
||||
|
||||
|
||||
def run_undelivered_compare():
|
||||
"""应到未到比对(全站点):调用 expected_undelivered,读 downloads/ 下的应到/实到
|
||||
@@ -279,399 +172,174 @@ def _print_test_report(results):
|
||||
print("====================================================")
|
||||
|
||||
|
||||
def run_multi_site_daemon():
|
||||
"""多站点自动化主控流程"""
|
||||
# ====================================================================
|
||||
# 交互菜单模式
|
||||
# ====================================================================
|
||||
|
||||
# 1. 读取配置文件
|
||||
|
||||
def _read_debug_config():
|
||||
"""读 config.yaml 的 debug 段,返回 (debug_mode, debug_target)。"""
|
||||
debug_mode = False
|
||||
debug_target = ""
|
||||
anneng_app_path = ""
|
||||
try:
|
||||
if os.path.exists(CONFIG_PATH):
|
||||
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
||||
config = yaml.safe_load(f) or {}
|
||||
debug_mode = (config.get("debug", {}) or {}).get("enabled", False)
|
||||
debug_target = (config.get("debug", {}) or {}).get("target_site", "")
|
||||
anneng_app_path = (config.get("anneng", {}) or {}).get("app_path", "")
|
||||
except Exception as e:
|
||||
print(f"⚠️ 读取 config.yaml 异常,将使用全量模式启动: {e}")
|
||||
return debug_mode, debug_target
|
||||
|
||||
# 动态确定需要挂载启动的网页站点;安能(Electron 应用)单独标记
|
||||
anneng_active = False
|
||||
if debug_mode:
|
||||
if debug_target in SITES_CONFIG:
|
||||
print(f"\n🛠️ 【调试模式】仅加载目标站点: [{debug_target}]")
|
||||
active_sites = {debug_target: SITES_CONFIG[debug_target]}
|
||||
elif debug_target == "安能":
|
||||
print(f"\n🛠️ 【调试模式】仅加载目标站点: [安能]")
|
||||
active_sites = {}
|
||||
anneng_active = True
|
||||
else:
|
||||
active_sites = dict(SITES_CONFIG)
|
||||
anneng_active = True
|
||||
else:
|
||||
active_sites = dict(SITES_CONFIG)
|
||||
anneng_active = True
|
||||
|
||||
if anneng_active and not anneng_app_path:
|
||||
print("⚠️ 已启用安能但 config.yaml 未配置 anneng.app_path,将跳过安能。")
|
||||
anneng_active = False
|
||||
# 菜单编号 → 任务规格(dispatch_task 消费)
|
||||
CHOICE_TO_TASK = {
|
||||
"1": {"site": "顺心", "kind": "expected"},
|
||||
"2": {"site": "顺心", "kind": "actual"},
|
||||
"3": {"site": "百世", "kind": "undelivered"},
|
||||
"4": {"site": "中通", "kind": "expected"},
|
||||
"5": {"site": "中通", "kind": "actual"},
|
||||
"6": {"site": "韵达", "kind": "expected"},
|
||||
"7": {"site": "韵达", "kind": "actual"},
|
||||
"10": {"site": "安能", "kind": "expected"},
|
||||
"11": {"site": "安能", "kind": "actual"},
|
||||
"9": {"site": "__compare__", "kind": "compare"},
|
||||
}
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=False)
|
||||
context = browser.new_context(viewport={"width": 1920, "height": 1080})
|
||||
|
||||
pages_map = {}
|
||||
def _interactive_menu_loop(ctx):
|
||||
"""交互菜单循环:input 后台线程 + _await_command + dispatch_task + 心跳 + 状态盘。
|
||||
|
||||
print("\n====================================================")
|
||||
print("【启动】正在打开各站点页面...")
|
||||
print("====================================================")
|
||||
所有 page 操作经 runtime(主线程),满足 Playwright sync 线程安全。
|
||||
"""
|
||||
pages_map = ctx.pages_map
|
||||
sites_to_watch = ctx.sites_to_watch
|
||||
|
||||
for site_name, url in active_sites.items():
|
||||
if site_name == "顺心":
|
||||
# 顺心:两个归属地账号在同一窗口各开一个标签页(顺心站点支持
|
||||
# 同浏览器双账号并存);pages_map["顺心"] 存为 page 列表。
|
||||
sx_pages = []
|
||||
for acct in range(1, 3):
|
||||
print(f">> 正在启动【顺心】账号{acct}标签页: {url}")
|
||||
sx_page = context.new_page()
|
||||
sx_page.goto(url)
|
||||
sx_pages.append(sx_page)
|
||||
pages_map["顺心"] = sx_pages
|
||||
else:
|
||||
print(f">> 正在启动【{site_name}】页面: {url}")
|
||||
page = context.new_page()
|
||||
page.goto(url)
|
||||
pages_map[site_name] = page
|
||||
def is_site_ready(site_name):
|
||||
if site_name not in pages_map:
|
||||
print(f"\n🚫 站点 [{site_name}] 未加载(当前为调试模式),已跳过。")
|
||||
return False
|
||||
return True
|
||||
|
||||
# 安能:以调试模式启动 Electron 应用(非 Playwright 网页)
|
||||
anneng_proc = None
|
||||
if anneng_active:
|
||||
try:
|
||||
anneng_proc = launch_anneng(anneng_app_path)
|
||||
pages_map["安能"] = True # 哨兵:表示已启动(无 Playwright page 对象)
|
||||
except Exception as e:
|
||||
print(f"⚠️ 启动安能应用失败,已跳过安能:{e}")
|
||||
anneng_active = False
|
||||
|
||||
print("\n====================================================")
|
||||
print("【登录检测】正在准备各站点登录...")
|
||||
print("====================================================")
|
||||
|
||||
# 针对支持纯代码自动登录的站点,在此处前置注入登录事件
|
||||
if "韵达" in pages_map:
|
||||
try:
|
||||
pages_map["韵达"].bring_to_front()
|
||||
site_yunda.yunda_login(pages_map["韵达"])
|
||||
except Exception as e:
|
||||
print(f" ⚠️ 韵达前置自动登录模块发生波动: {e}")
|
||||
|
||||
# ====================================================================
|
||||
# 就绪轮询 (Ready Guard Polling)
|
||||
# 自动识别各站点登录完成状态,无需手动回车
|
||||
# ====================================================================
|
||||
ready_status = {site: False for site in active_sites.keys()}
|
||||
if anneng_active:
|
||||
ready_status["安能"] = False
|
||||
|
||||
print("\n>> 正在轮询各站点就绪状态 (自动登录或手动登录均可)...")
|
||||
while not all(ready_status.values()):
|
||||
for site_name in list(ready_status.keys()):
|
||||
if ready_status[site_name]:
|
||||
continue
|
||||
try:
|
||||
if site_name == "安能":
|
||||
# 安能走 CDP 判断主页是否就绪(连不上返回 False,不抛异常)
|
||||
ok = site_anneng.anneng_ready()
|
||||
elif site_name == "顺心":
|
||||
# 顺心双账号:两个 page 都进主页才算就绪
|
||||
ok = all(
|
||||
pg.locator(READY_SELECTORS["顺心"]).is_visible(timeout=500)
|
||||
for pg in pages_map["顺心"]
|
||||
)
|
||||
else:
|
||||
# 0.5 秒轻量探测,避免阻塞主循环
|
||||
ok = (
|
||||
pages_map[site_name]
|
||||
.locator(READY_SELECTORS[site_name])
|
||||
.is_visible(timeout=500)
|
||||
)
|
||||
if ok:
|
||||
ready_status[site_name] = True
|
||||
print(f" ✅ 【{site_name}】已检测到主页,登录就绪。")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
pending_sites = [s for s, ready in ready_status.items() if not ready]
|
||||
if pending_sites:
|
||||
print(
|
||||
f" ⏳ 等待以下站点完成登录: [{', '.join(pending_sites)}] ... "
|
||||
"(请在浏览器/应用中操作)"
|
||||
)
|
||||
time.sleep(3) # 等待 3 秒后进行下一轮检查
|
||||
|
||||
print("\n====================================================")
|
||||
print("【准备】所有站点已就绪,正在清理初始弹窗...")
|
||||
print("====================================================")
|
||||
|
||||
# 顺心:处理初始弹窗(双账号两个 page 各处理一遍)
|
||||
if "顺心" in pages_map:
|
||||
for acct, sx_page in enumerate(pages_map["顺心"], start=1):
|
||||
try:
|
||||
sx_page.bring_to_front()
|
||||
print(f">> 正在处理【顺心】账号{acct}弹窗与遮罩...")
|
||||
sx_page.locator("a").nth(4).click(timeout=2000)
|
||||
sx_page.wait_for_timeout(500)
|
||||
sx_page.get_by_role("button", name="Close").click(timeout=2000)
|
||||
sx_page.wait_for_timeout(500)
|
||||
sx_page.get_by_role("button", name="不再询问").click(timeout=2000)
|
||||
print(f" ✅ 【顺心】账号{acct}初始弹窗处理完成。")
|
||||
except Exception:
|
||||
pass # 环境可能很干净没有弹窗,无视报错
|
||||
|
||||
# 百世:循环关闭初始弹窗
|
||||
if "百世" in pages_map:
|
||||
try:
|
||||
bs_page = pages_map["百世"]
|
||||
bs_page.bring_to_front()
|
||||
print(">> 正在处理【百世】阅读完毕与关闭按钮...")
|
||||
for round_idx in range(4):
|
||||
handled_any = False
|
||||
try:
|
||||
read_btns = bs_page.locator("button:has-text('阅读完毕')")
|
||||
if read_btns.count() > 0:
|
||||
for i in range(read_btns.count()):
|
||||
if read_btns.nth(i).is_visible(timeout=500):
|
||||
read_btns.nth(i).click()
|
||||
handled_any = True
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if bs_page.locator("button:has-text('关 闭')").is_visible(
|
||||
timeout=500
|
||||
):
|
||||
bs_page.locator("button:has-text('关 闭')").click()
|
||||
handled_any = True
|
||||
except Exception:
|
||||
pass
|
||||
if not handled_any:
|
||||
break
|
||||
bs_page.wait_for_timeout(800)
|
||||
print(" ✅ 【百世】初始弹窗处理完成。")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if "中通" in pages_map:
|
||||
print(" ✅ 【中通】已就绪。")
|
||||
|
||||
if "韵达" in pages_map:
|
||||
print(" ✅ 【韵达】已就绪。")
|
||||
|
||||
if "安能" in pages_map:
|
||||
print(" ✅ 【安能】已就绪。")
|
||||
|
||||
def is_site_ready(site_name):
|
||||
if site_name not in pages_map:
|
||||
print(f"\n🚫 站点 [{site_name}] 未加载(当前为调试模式),已跳过。")
|
||||
return False
|
||||
return True
|
||||
|
||||
# ==================================================================
|
||||
# 阶段0:后台心跳 + 状态持久化
|
||||
# input 放后台线程(只读 stdin,不碰 Playwright),主线程在 _await_command
|
||||
# 里轮询命令队列并定期跑心跳;所有 page 操作仍在主线程,满足 Playwright
|
||||
# sync 的线程安全。
|
||||
# ==================================================================
|
||||
state_store.init_db()
|
||||
sites_to_watch = list(ready_status.keys())
|
||||
# 就绪轮询刚通过 → 各站视为已登录写一次初值,后续心跳校正
|
||||
for _site in sites_to_watch:
|
||||
state_store.set_login_state(_site, True)
|
||||
|
||||
last_heartbeat = 0.0
|
||||
|
||||
def _run_heartbeat():
|
||||
"""一轮心跳:探测各站登录态 + 数据文件,写状态库;登录态变化时提示。"""
|
||||
nonlocal last_heartbeat
|
||||
prev = state_store.get_all_status()
|
||||
for site_name in sites_to_watch:
|
||||
logged_in = probe_site_login(site_name, pages_map)
|
||||
prev_login = prev.get(site_name, {}).get("login_state")
|
||||
state_store.set_login_state(site_name, logged_in)
|
||||
now_login = state_store.LOGIN_IN if logged_in else state_store.LOGIN_OUT
|
||||
if prev_login and prev_login not in (
|
||||
now_login,
|
||||
state_store.LOGIN_UNKNOWN,
|
||||
):
|
||||
print(
|
||||
f"\n ⚠️【{site_name}】登录态变化: {prev_login} → {now_login}"
|
||||
)
|
||||
for kind in ("expected", "actual"):
|
||||
ready, gen_at = probe_data_file(site_name, kind)
|
||||
state_store.set_data_state(site_name, kind, ready, gen_at)
|
||||
last_heartbeat = time.monotonic()
|
||||
|
||||
def _print_status_board():
|
||||
"""打印各站登录态 / 数据就绪状态盘(菜单 [12])。"""
|
||||
print("\n====================== 站点状态盘 ======================")
|
||||
status = state_store.get_all_status()
|
||||
if not status:
|
||||
print(" (暂无状态记录)")
|
||||
print("======================================================")
|
||||
return
|
||||
login_text = {
|
||||
state_store.LOGIN_IN: "✅ 已登录",
|
||||
state_store.LOGIN_OUT: "❌ 未登录",
|
||||
state_store.LOGIN_UNKNOWN: "❔ 未知",
|
||||
}
|
||||
for site_name in sites_to_watch:
|
||||
s = status.get(site_name)
|
||||
if not s:
|
||||
continue
|
||||
login_mark = login_text.get(s["login_state"], s["login_state"])
|
||||
exp = (
|
||||
f"应到{'✅' if s['expected_ready'] else '—'} "
|
||||
f"{s['expected_generated_at'] or '无'}"
|
||||
)
|
||||
act = (
|
||||
f"实到{'✅' if s['actual_ready'] else '—'} "
|
||||
f"{s['actual_generated_at'] or '无'}"
|
||||
)
|
||||
print(
|
||||
f" 【{site_name}】 {login_mark} | {exp} | {act} "
|
||||
f"| 探测于 {s['login_checked_at']}"
|
||||
)
|
||||
print("======================================================")
|
||||
|
||||
command_queue = queue.Queue()
|
||||
|
||||
def _input_loop():
|
||||
"""后台线程:读用户输入塞进队列。不碰任何 Playwright 对象。"""
|
||||
while True:
|
||||
try:
|
||||
command_queue.put(input())
|
||||
except EOFError:
|
||||
return
|
||||
|
||||
threading.Thread(target=_input_loop, daemon=True).start()
|
||||
|
||||
def _await_command():
|
||||
"""等待一条命令(期间跑心跳)。命令到来即返回。"""
|
||||
nonlocal last_heartbeat
|
||||
while True:
|
||||
try:
|
||||
return command_queue.get(timeout=0.5)
|
||||
except queue.Empty:
|
||||
if time.monotonic() - last_heartbeat >= HEARTBEAT_INTERVAL:
|
||||
_run_heartbeat()
|
||||
last_heartbeat = 0.0
|
||||
command_queue = queue.Queue()
|
||||
|
||||
def _await_command():
|
||||
nonlocal last_heartbeat
|
||||
while True:
|
||||
print("\n====================================================")
|
||||
print(" 物流数据下载主菜单 ")
|
||||
if debug_mode:
|
||||
print(f" [ 调试模式,仅加载: {debug_target} ]")
|
||||
print("====================================================")
|
||||
print(" 模块一:【顺心】数据处理流")
|
||||
print(" [1] 执行 - 应到货物数据下载")
|
||||
print(" [2] 执行 - 实到货物数据下载")
|
||||
print("-" * 52)
|
||||
print(" 模块二:【百世】数据处理流")
|
||||
print(" [3] 执行 - 一键提取应到未到异常数据")
|
||||
print("-" * 52)
|
||||
print(" 模块三:【中通】数据处理流")
|
||||
print(" [4] 执行 - 应到货物数据下载")
|
||||
print(" [5] 执行 - 实到货物数据下载")
|
||||
print("-" * 52)
|
||||
print(" 模块四:【韵达】数据处理流")
|
||||
print(" [6] 执行 - 应到货物数据下载")
|
||||
print(" [7] 执行 - 实到货物数据下载")
|
||||
print("-" * 52)
|
||||
print(" 模块五:【安能】数据处理流(Electron 应用)")
|
||||
print(" [10] 执行 - 应到货物数据下载(运单信息)")
|
||||
print(" [11] 执行 - 实到货物数据下载(网点到件扫描)")
|
||||
print("-" * 52)
|
||||
print(" 自动化测试")
|
||||
print(" [8] 执行 - 全站点下载流程自动化测试 (交叉跑通校验)")
|
||||
print("-" * 52)
|
||||
print(" 全局离线数据处理")
|
||||
try:
|
||||
return command_queue.get(timeout=0.5)
|
||||
except queue.Empty:
|
||||
if time.monotonic() - last_heartbeat >= HEARTBEAT_INTERVAL:
|
||||
run_heartbeat(ctx)
|
||||
last_heartbeat = time.monotonic()
|
||||
|
||||
def _print_status_board():
|
||||
print("\n====================== 站点状态盘 ======================")
|
||||
status = state_store.get_all_status()
|
||||
if not status:
|
||||
print(" (暂无状态记录)")
|
||||
print("======================================================")
|
||||
return
|
||||
login_text = {
|
||||
state_store.LOGIN_IN: "✅ 已登录",
|
||||
state_store.LOGIN_OUT: "❌ 未登录",
|
||||
state_store.LOGIN_UNKNOWN: "❔ 未知",
|
||||
}
|
||||
for site_name in sites_to_watch:
|
||||
s = status.get(site_name)
|
||||
if not s:
|
||||
continue
|
||||
login_mark = login_text.get(s["login_state"], s["login_state"])
|
||||
exp = f"应到{'✅' if s['expected_ready'] else '—'} {s['expected_generated_at'] or '无'}"
|
||||
act = f"实到{'✅' if s['actual_ready'] else '—'} {s['actual_generated_at'] or '无'}"
|
||||
print(
|
||||
" [9] 执行 - 应到未到比对(全站点汇总,输出 output/应到未到数据.xlsx)"
|
||||
f" 【{site_name}】 {login_mark} | {exp} | {act} "
|
||||
f"| 探测于 {s['login_checked_at']}"
|
||||
)
|
||||
print("-" * 52)
|
||||
print(" 站点状态")
|
||||
print(" [12] 查看 - 各站登录态 / 数据就绪状态")
|
||||
print("-" * 52)
|
||||
print(" [0] 退出系统")
|
||||
print("====================================================")
|
||||
|
||||
print("请输入任务编号并回车: ", end="", flush=True)
|
||||
choice = _await_command()
|
||||
print("======================================================")
|
||||
|
||||
def _input_loop():
|
||||
while True:
|
||||
try:
|
||||
if choice == "1" and is_site_ready("顺心"):
|
||||
site_shunxin.shunxin_expected_download(pages_map["顺心"])
|
||||
elif choice == "2" and is_site_ready("顺心"):
|
||||
site_shunxin.shunxin_actual_download(pages_map["顺心"])
|
||||
elif choice == "3" and is_site_ready("百世"):
|
||||
page = pages_map["百世"]
|
||||
page.bring_to_front()
|
||||
site_baishi.baishi_download_undelivered_data(page)
|
||||
elif choice == "4" and is_site_ready("中通"):
|
||||
page = pages_map["中通"]
|
||||
page.bring_to_front()
|
||||
site_zto.zto_expected_download(page)
|
||||
elif choice == "5" and is_site_ready("中通"):
|
||||
page = pages_map["中通"]
|
||||
page.bring_to_front()
|
||||
site_zto.zto_actual_download(page)
|
||||
elif choice == "6" and is_site_ready("韵达"):
|
||||
page = pages_map["韵达"]
|
||||
page.bring_to_front()
|
||||
site_yunda.yunda_expected_download(page)
|
||||
elif choice == "7" and is_site_ready("韵达"):
|
||||
page = pages_map["韵达"]
|
||||
page.bring_to_front()
|
||||
site_yunda.yunda_actual_download(page)
|
||||
elif choice == "10" and is_site_ready("安能"):
|
||||
site_anneng.anneng_expected_download()
|
||||
elif choice == "11" and is_site_ready("安能"):
|
||||
site_anneng.anneng_actual_download()
|
||||
elif choice == "8":
|
||||
run_automation_test(pages_map)
|
||||
elif choice == "9":
|
||||
run_undelivered_compare()
|
||||
elif choice == "12":
|
||||
_print_status_board()
|
||||
elif choice == "0":
|
||||
print("\n正在关闭浏览器并退出...")
|
||||
break
|
||||
else:
|
||||
if choice not in [
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"10",
|
||||
"11",
|
||||
"12",
|
||||
"0",
|
||||
]:
|
||||
print("\n⚠️ 无效输入,请查证后回车。")
|
||||
except Exception as e:
|
||||
print(f"❌ 任务调度异常: {e}")
|
||||
command_queue.put(input())
|
||||
except EOFError:
|
||||
return
|
||||
|
||||
browser.close()
|
||||
if anneng_proc is not None:
|
||||
try:
|
||||
anneng_proc.terminate()
|
||||
print("已关闭安能应用。")
|
||||
except Exception:
|
||||
pass
|
||||
threading.Thread(target=_input_loop, daemon=True).start()
|
||||
|
||||
while True:
|
||||
print("\n====================================================")
|
||||
print(" 物流数据下载主菜单 ")
|
||||
if ctx.debug_mode:
|
||||
print(f" [ 调试模式,仅加载: {ctx.debug_target} ]")
|
||||
print("====================================================")
|
||||
print(" 模块一:【顺心】数据处理流")
|
||||
print(" [1] 执行 - 应到货物数据下载")
|
||||
print(" [2] 执行 - 实到货物数据下载")
|
||||
print("-" * 52)
|
||||
print(" 模块二:【百世】数据处理流")
|
||||
print(" [3] 执行 - 一键提取应到未到异常数据")
|
||||
print("-" * 52)
|
||||
print(" 模块三:【中通】数据处理流")
|
||||
print(" [4] 执行 - 应到货物数据下载")
|
||||
print(" [5] 执行 - 实到货物数据下载")
|
||||
print("-" * 52)
|
||||
print(" 模块四:【韵达】数据处理流")
|
||||
print(" [6] 执行 - 应到货物数据下载")
|
||||
print(" [7] 执行 - 实到货物数据下载")
|
||||
print("-" * 52)
|
||||
print(" 模块五:【安能】数据处理流(Electron 应用)")
|
||||
print(" [10] 执行 - 应到货物数据下载(运单信息)")
|
||||
print(" [11] 执行 - 实到货物数据下载(网点到件扫描)")
|
||||
print("-" * 52)
|
||||
print(" 自动化测试")
|
||||
print(" [8] 执行 - 全站点下载流程自动化测试 (交叉跑通校验)")
|
||||
print("-" * 52)
|
||||
print(" 全局离线数据处理")
|
||||
print(" [9] 执行 - 应到未到比对(全站点汇总,输出 output/应到未到数据.xlsx)")
|
||||
print("-" * 52)
|
||||
print(" 站点状态")
|
||||
print(" [12] 查看 - 各站登录态 / 数据就绪状态")
|
||||
print("-" * 52)
|
||||
print(" [0] 退出系统")
|
||||
print("====================================================")
|
||||
|
||||
print("请输入任务编号并回车: ", end="", flush=True)
|
||||
choice = _await_command()
|
||||
|
||||
try:
|
||||
if choice in CHOICE_TO_TASK:
|
||||
task = CHOICE_TO_TASK[choice]
|
||||
site = task["site"]
|
||||
if site == "__compare__" or is_site_ready(site):
|
||||
status, error = dispatch_task(ctx, task)
|
||||
if status == state_store.TASK_FAILED:
|
||||
print(f"❌ 任务失败: {error}")
|
||||
elif choice == "8":
|
||||
run_automation_test(pages_map)
|
||||
elif choice == "12":
|
||||
_print_status_board()
|
||||
elif choice == "0":
|
||||
break
|
||||
elif choice.strip() != "":
|
||||
print("\n⚠️ 无效输入,请查证后回车。")
|
||||
except Exception as e:
|
||||
print(f"❌ 任务调度异常: {e}")
|
||||
|
||||
|
||||
def run_multi_site_daemon():
|
||||
"""多站点自动化主控流程(交互菜单模式)。
|
||||
|
||||
启动 → 等待各站登录就绪 → 进入交互菜单;退出时关闭浏览器与安能。
|
||||
"""
|
||||
debug_mode, debug_target = _read_debug_config()
|
||||
ctx = launch_and_prepare(debug_mode, debug_target)
|
||||
try:
|
||||
_interactive_menu_loop(ctx)
|
||||
finally:
|
||||
print("\n正在关闭浏览器并退出...")
|
||||
ctx.stop()
|
||||
print("程序已退出。")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user