236 lines
6.7 KiB
Python
236 lines
6.7 KiB
Python
"""发票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 |