Files
verify-invoice-default/scripts/service/rpa/invoice_rpa.py
chenfuqiang c5103e2650
All checks were successful
技能自动化发布 / release (push) Successful in 9s
chore: auto release commit (2026-09-14 15:11:41)
2026-09-14 15:11:41 +08:00

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