autotrain/nextgen/autotrain_next/invoice_tools.py
2026-07-24 10:37:32 +08:00

164 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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.

"""H5 开票领域原子工具:每个工具只承担一个页面动作,可独立替换和验证。"""
from __future__ import annotations
import time
from pathlib import Path
import re
from . import device, ledger, profiles, vision
from .paths import OUTPUT
from .tools import ToolError, observe_screen, observe_xml, tap_normalized, input_text, swipe_normalized, back
def _image(context: dict) -> str:
result = observe_screen({}, context)
return str(result["screenshot"])
def _current(context: dict) -> dict:
current = context.setdefault("invoice", {}).get("current")
if not current:
raise ToolError("没有当前乘客;必须先从列表选择")
return current
def _scroll_up(context: dict) -> dict:
profile = profiles.load(str(context.get("device_serial") or "")) or {}
gesture = profile.get("gestures", {}).get("scroll_up")
if not isinstance(gesture, dict):
raise ToolError("设备档案缺少 gestures.scroll_up请重新采样或补充该设备配置")
return swipe_normalized(gesture, context)
def inspect_page(_: dict, context: dict) -> dict:
image = _image(context)
parsed = vision.detect_page(Path(image))
context.setdefault("invoice", {})["page"] = parsed.get("page", "other")
return {"screenshot": image, **parsed}
def open_next(_: dict, context: dict) -> dict:
image = _image(context)
rows = vision.list_rows(Path(image))
if not rows["page_ok"]:
raise ToolError("当前页面不是可识别的扫码开票单列表")
roster = ledger.read_roster(context.get("roster_path", ""))
for row in rows["rows"]:
if row.get("button_text") != "开具":
continue
matches = ledger.match(roster, str(row.get("masked_name", "")), str(row.get("id_first", "")), str(row.get("id_last", "")))
if len(matches) != 1:
continue
previous = ledger.progress_by_masked(context, str(row.get("masked_name", "")))
if previous and previous.get("status") in {"completed", "no_ticket"}:
continue
person = matches[0]
context.setdefault("invoice", {})["current"] = {"masked_name": row["masked_name"], "real_name": person["姓名"], "id_last8": person["身份证后8位"]}
ledger.record(context, person, row["masked_name"], "in_progress")
return {"selected": row["masked_name"], **tap_normalized({"point": row["point"]}, context)}
if rows.get("scroll_position") == "bottom":
context.setdefault("invoice", {})["finished"] = True
return {"finished": True, "reason": "没有待开具乘客"}
return {"scrolled": True, **_scroll_up(context)}
def _click_label(context: dict, text: str) -> dict:
image = _image(context)
target = vision.find_button(Path(image), text)
if not target.get("found"):
raise ToolError(f"视觉未找到「{text}」按钮")
return tap_normalized({"point": target["point"]}, context)
def verify(_: dict, context: dict) -> dict:
current = _current(context)
image = _image(context)
input_target = vision.find_button(Path(image), "证件号码")
if not input_target.get("found"):
raise ToolError("视觉未找到证件号码输入框")
tap_normalized({"point": input_target["point"]}, context)
input_text({"text": current["id_last8"]}, context)
return _click_label(context, "核验")
def submit_info(_: dict, context: dict) -> dict:
return _click_label(context, "提交")
def confirm(_: dict, context: dict) -> dict:
return _click_label(context, "确认")
def _remote_downloads(context: dict) -> set[str]:
command = "find /sdcard/Download -type f \\( -iname '*.pdf' -o -iname '*.ofd' -o -iname '*.xml' \\)"
output = device.run(str(context["device_serial"]), ["shell", "sh", "-c", command], check=False).stdout
return {line.strip() for line in str(output).splitlines() if line.strip().startswith("/")}
def download_and_continue(_: dict, context: dict) -> dict:
"""在成功页下载到 NextGen 输出区,再记录完成并返回列表。"""
current = _current(context)
before = _remote_downloads(context)
_click_label(context, "发票预览/下载")
deadline = time.monotonic() + 30
downloaded: set[str] = set()
while time.monotonic() < deadline:
texts = observe_xml({}, context).get("texts", [])
joined = "\n".join(texts)
if "是否允许" in joined and "允许" in joined:
_click_label(context, "允许")
elif "下载发票PDF" in joined:
_click_label(context, "下载发票PDF")
elif "下载发票" in joined:
_click_label(context, "下载发票")
elif "下载进度" in joined and "100%" in joined and "确定" in joined:
_click_label(context, "确定")
downloaded = _remote_downloads(context) - before
if downloaded:
break
time.sleep(1)
if not downloaded:
raise ToolError("未在手机 Download 目录发现新的发票文件")
remote = sorted(downloaded)[-1]
suffix = Path(remote).suffix.lower() or ".pdf"
OUTPUT.mkdir(parents=True, exist_ok=True)
local = OUTPUT / f"{context.get('id', 'invoice')}_{time.time_ns()}{suffix}"
device.run(str(context["device_serial"]), ["pull", remote, str(local)])
ledger.record(context, {"姓名": current["real_name"], "身份证后8位": current["id_last8"]}, current["masked_name"], "completed", str(local))
_click_label(context, "继续开票")
context.setdefault("invoice", {}).pop("current", None)
return {"remote": remote, "local": str(local)}
def send_email_and_continue(_: dict, context: dict) -> dict:
"""按旧流程的“发送至邮箱”结果路径处理;邮箱地址仍由 12306 已保存信息决定。"""
current = _current(context)
_click_label(context, "发送至邮箱")
deadline = time.monotonic() + 20
while time.monotonic() < deadline:
texts = observe_xml({}, context).get("texts", [])
joined = "\n".join(texts)
if "邮件发送完成" in joined:
break
if "邮箱地址确认" in joined:
_click_label(context, "确认")
time.sleep(1)
else:
raise ToolError("等待邮件发送完成超时")
ledger.record(context, {"姓名": current["real_name"], "身份证后8位": current["id_last8"]}, current["masked_name"], "completed", "已发送至邮箱")
_click_label(context, "继续开票")
context.setdefault("invoice", {}).pop("current", None)
return {"sent": True}
def mark_no_ticket(_: dict, context: dict) -> dict:
current = _current(context)
ledger.record(context, {"姓名": current["real_name"], "身份证后8位": current["id_last8"]}, current["masked_name"], "no_ticket")
context.setdefault("invoice", {}).pop("current", None)
return back({}, context)
def wait_loading(arguments: dict, context: dict) -> dict:
time.sleep(max(0.2, min(float(arguments.get("seconds", 2)), 8)))
return inspect_page({}, context)