"""发票RPA主流程:协调登录、导航、提取等操作。""" from __future__ import annotations import json import logging import uuid from typing import Any, Dict, Optional from jiangchang_skill_core.activity import interruptible_sleep, rpa_step from jiangchang_skill_core.rpa.video_session import RpaVideoSession from .browser_session import close_browser_context, start_browser_session from .invoice_extractor import extract_invoice_detail, extract_invoice_list, format_invoice_data from .login_handler import login_if_needed logger = logging.getLogger(__name__) class InvoiceRpaResult: """RPA执行结果。""" def __init__( self, success: bool, data: Optional[Dict[str, Any]] = None, error: Optional[str] = None, stage: Optional[str] = None, ): self.success = success self.data = data or {} self.error = error self.stage = stage def to_dict(self) -> Dict[str, Any]: return { "success": self.success, "data": self.data, "error": self.error, "stage": self.stage, } @rpa_step("启动浏览器并登录") async def _start_and_login( profile_dir: str, username: str, password: str, headless: bool = False, ) -> tuple[Any, Any, Any, bool]: """启动浏览器并登录。""" pw, context, page = await start_browser_session(profile_dir, headless=headless) # 登录 login_success = await login_if_needed(page, username, password) if not login_success: await close_browser_context(pw, context) raise RuntimeError("ERROR:LOGIN_TIMEOUT 登录失败或超时") return pw, context, page, True @rpa_step("导航到发票预检页面") async def _navigate_to_invoice_page(page: Any) -> bool: """导航到发票预检页面。""" try: # 检查当前页面是否已在目标页面 if "invoice-precheck" in page.url: logger.info("already_on_invoice_page url=%s", page.url) return True # 导航到目标页面 target_url = "https://sandbox.jc2009.com/industries/campus/smartcampus/app/finance/invoice-precheck" await page.goto(target_url, wait_until="domcontentloaded", timeout=60000) # 等待页面加载 await interruptible_sleep(3.0) logger.info("navigated_to_invoice_page url=%s", page.url) return True except Exception as e: logger.error("navigation_failed error=%s", str(e)) return False @rpa_step("提取发票信息") async def _extract_invoices(page: Any, detail_mode: bool = False) -> Dict[str, Any]: """提取发票信息。""" try: if detail_mode: # 提取详细信息 invoices = [] rows = await page.locator("tr").all() for i in range(len(rows)): detail = await extract_invoice_detail(page, i) if detail: invoices.append(detail) return format_invoice_data(invoices) else: # 提取列表信息 invoices = await extract_invoice_list(page) return format_invoice_data(invoices) except Exception as e: logger.error("extraction_failed error=%s", str(e)) return {"error": str(e)} async def run_invoice_rpa( username: str, password: str, profile_dir: str, headless: bool = False, detail_mode: bool = False, data_dir: str = "", batch_id: str = "", ) -> InvoiceRpaResult: """运行发票RPA主流程。 Args: username: 用户名 password: 密码 profile_dir: 浏览器配置目录 headless: 是否无头模式 detail_mode: 是否提取详细信息 data_dir: 数据目录 batch_id: 批次ID Returns: InvoiceRpaResult: 执行结果 """ if not batch_id: batch_id = uuid.uuid4().hex[:12] logger.info( "invoice_rpa_start username=%s batch_id=%s detail_mode=%s", username[:3] + "***", # 脱敏 batch_id, detail_mode, ) pw = None context = None page = None try: # 启动浏览器并登录 pw, context, page, _ = await _start_and_login( profile_dir, username, password, headless ) # 导航到发票页面 if not await _navigate_to_invoice_page(page): return InvoiceRpaResult( success=False, error="导航到发票页面失败", stage="navigation", ) # 提取发票信息 result_data = await _extract_invoices(page, detail_mode) if "error" in result_data: return InvoiceRpaResult( success=False, error=result_data["error"], stage="extraction", ) logger.info( "invoice_rpa_success total_invoices=%s batch_id=%s", result_data.get("total_count", 0), batch_id, ) return InvoiceRpaResult( success=True, data=result_data, stage="completed", ) except Exception as e: logger.error("invoice_rpa_failed error=%s batch_id=%s", str(e), batch_id) return InvoiceRpaResult( success=False, error=str(e), stage="unknown", ) finally: # 关闭浏览器 if pw and context: await close_browser_context(pw, context) async def run_invoice_rpa_with_video( username: str, password: str, profile_dir: str, skill_slug: str, skill_data_dir: str, headless: bool = False, detail_mode: bool = False, title: str = "开始提取发票信息", closing_title: str = "发票信息提取完成", ) -> InvoiceRpaResult: """带录屏的发票RPA主流程。""" batch_id = uuid.uuid4().hex[:12] async with RpaVideoSession( skill_slug=skill_slug, skill_data_dir=skill_data_dir, batch_id=batch_id, title=title, closing_title=closing_title, ) as video: video.add_step("准备启动浏览器") # 运行RPA result = await run_invoice_rpa( username=username, password=password, profile_dir=profile_dir, headless=headless, detail_mode=detail_mode, data_dir=skill_data_dir, batch_id=batch_id, ) if result.success: video.add_step(f"成功提取 {result.data.get('total_count', 0)} 张发票信息") else: video.add_step(f"提取失败: {result.error}") return result