278 lines
7.9 KiB
Python
278 lines
7.9 KiB
Python
"""发票RPA主流程:协调登录、导航、提取等操作。
|
||
|
||
完整流程:
|
||
1. 启动浏览器
|
||
2. 登录 sandbox.jc2009.com(手机号+密码)
|
||
3. 找到"智慧校园"并点击进入
|
||
4. 登录智慧校园(默认 demo 账号)
|
||
5. 导航到票据合规预审页面
|
||
6. 提取发票数据
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
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_form_fields, extract_invoice_list, format_invoice_data
|
||
from .login_handler import (
|
||
enter_smart_campus,
|
||
login_sandbox,
|
||
login_smart_campus,
|
||
navigate_to_invoice_precheck,
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def _get_active_page(context: Any) -> Any:
|
||
"""获取 context 中最新的活跃页面(处理新 tab 场景)。"""
|
||
pages = context.pages
|
||
if pages:
|
||
return pages[-1]
|
||
raise RuntimeError("ERROR:NO_PAGE 浏览器没有可用页面")
|
||
|
||
|
||
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_browser(profile_dir: str, headless: bool = False) -> tuple[Any, Any, Any]:
|
||
"""启动浏览器会话。"""
|
||
pw, context, page = await start_browser_session(profile_dir, headless=headless)
|
||
return pw, context, page
|
||
|
||
|
||
@rpa_step("登录 sandbox 门户")
|
||
async def _login_sandbox_portal(page: Any, phone: str, password: str) -> bool:
|
||
"""登录 sandbox.jc2009.com 门户。"""
|
||
return await login_sandbox(page, phone, password)
|
||
|
||
|
||
@rpa_step("进入智慧校园")
|
||
async def _enter_campus(page: Any) -> bool:
|
||
"""在平台首页找到并点击智慧校园。"""
|
||
return await enter_smart_campus(page)
|
||
|
||
|
||
@rpa_step("登录智慧校园")
|
||
async def _login_campus(page: Any) -> bool:
|
||
"""登录智慧校园系统。"""
|
||
return await login_smart_campus(page)
|
||
|
||
|
||
@rpa_step("导航到票据合规预审")
|
||
async def _navigate_to_invoice(page: Any) -> bool:
|
||
"""导航到票据合规预审页面。"""
|
||
return await navigate_to_invoice_precheck(page)
|
||
|
||
|
||
@rpa_step("提取发票数据")
|
||
async def _extract_data(page: Any) -> Dict[str, Any]:
|
||
"""提取发票列表和表单字段。"""
|
||
# 提取表单字段
|
||
form_fields = await extract_form_fields(page)
|
||
logger.info("form_fields_extracted keys=%s", list(form_fields.keys()))
|
||
|
||
# 提取发票列表
|
||
invoices = await extract_invoice_list(page)
|
||
logger.info("invoices_extracted count=%s", len(invoices))
|
||
|
||
# 组合结果
|
||
result = format_invoice_data(invoices)
|
||
result.update(form_fields)
|
||
return result
|
||
|
||
|
||
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: 手机号(sandbox 门户登录)
|
||
password: 密码(sandbox 门户登录)
|
||
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 or "")[:3],
|
||
batch_id,
|
||
detail_mode,
|
||
)
|
||
|
||
pw = None
|
||
context = None
|
||
page = None
|
||
|
||
try:
|
||
# 步骤 1:启动浏览器
|
||
pw, context, page = await _start_browser(profile_dir, headless)
|
||
|
||
# 步骤 2:登录 sandbox 门户
|
||
sandbox_ok = await _login_sandbox_portal(page, username, password)
|
||
if not sandbox_ok:
|
||
return InvoiceRpaResult(
|
||
success=False,
|
||
error="登录 sandbox 门户失败",
|
||
stage="sandbox_login",
|
||
)
|
||
|
||
# 步骤 3:进入智慧校园(可能打开新 tab,需要刷新 page)
|
||
campus_ok = await _enter_campus(page)
|
||
if not campus_ok:
|
||
return InvoiceRpaResult(
|
||
success=False,
|
||
error="进入智慧校园失败",
|
||
stage="enter_campus",
|
||
)
|
||
|
||
# 刷新 page 引用(处理新 tab 场景)
|
||
page = _get_active_page(context)
|
||
logger.info("page_refreshed_after_enter_campus url=%s", page.url)
|
||
|
||
# 步骤 4:登录智慧校园
|
||
login_ok = await _login_campus(page)
|
||
if not login_ok:
|
||
return InvoiceRpaResult(
|
||
success=False,
|
||
error="登录智慧校园失败",
|
||
stage="campus_login",
|
||
)
|
||
|
||
# 刷新 page 引用
|
||
page = _get_active_page(context)
|
||
logger.info("page_refreshed_after_campus_login url=%s", page.url)
|
||
|
||
# 步骤 5:导航到票据合规预审
|
||
nav_ok = await _navigate_to_invoice(page)
|
||
if not nav_ok:
|
||
return InvoiceRpaResult(
|
||
success=False,
|
||
error="导航到票据合规预审页面失败",
|
||
stage="navigation",
|
||
)
|
||
|
||
# 刷新 page 引用
|
||
page = _get_active_page(context)
|
||
logger.info("page_refreshed_after_navigate url=%s", page.url)
|
||
|
||
# 步骤 6:提取发票数据
|
||
result_data = await _extract_data(page)
|
||
|
||
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
|