chore: auto release commit (2026-09-14 22:22:49)
All checks were successful
技能自动化发布 / release (push) Successful in 7s
All checks were successful
技能自动化发布 / release (push) Successful in 7s
This commit is contained in:
@@ -1,8 +1,16 @@
|
||||
"""发票RPA主流程:协调登录、导航、提取等操作。"""
|
||||
"""发票RPA主流程:协调登录、导航、提取等操作。
|
||||
|
||||
完整流程:
|
||||
1. 启动浏览器
|
||||
2. 登录 sandbox.jc2009.com(手机号+密码)
|
||||
3. 找到"智慧校园"并点击进入
|
||||
4. 登录智慧校园(默认 demo 账号)
|
||||
5. 导航到票据合规预审页面
|
||||
6. 提取发票数据
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Any, Dict, Optional
|
||||
@@ -11,15 +19,28 @@ 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
|
||||
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,
|
||||
@@ -31,7 +52,7 @@ class InvoiceRpaResult:
|
||||
self.data = data or {}
|
||||
self.error = error
|
||||
self.stage = stage
|
||||
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"success": self.success,
|
||||
@@ -41,70 +62,52 @@ class InvoiceRpaResult:
|
||||
}
|
||||
|
||||
|
||||
@rpa_step("启动浏览器并登录")
|
||||
async def _start_and_login(
|
||||
profile_dir: str,
|
||||
username: str,
|
||||
password: str,
|
||||
headless: bool = False,
|
||||
) -> tuple[Any, Any, Any, bool]:
|
||||
"""启动浏览器并登录。"""
|
||||
@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)
|
||||
|
||||
# 登录
|
||||
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
|
||||
return pw, context, page
|
||||
|
||||
|
||||
@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("登录 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 _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)}
|
||||
@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(
|
||||
@@ -117,69 +120,107 @@ async def run_invoice_rpa(
|
||||
batch_id: str = "",
|
||||
) -> InvoiceRpaResult:
|
||||
"""运行发票RPA主流程。
|
||||
|
||||
|
||||
Args:
|
||||
username: 用户名
|
||||
password: 密码
|
||||
username: 手机号(sandbox 门户登录)
|
||||
password: 密码(sandbox 门户登录)
|
||||
profile_dir: 浏览器配置目录
|
||||
headless: 是否无头模式
|
||||
detail_mode: 是否提取详细信息
|
||||
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] + "***", # 脱敏
|
||||
"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:
|
||||
# 启动浏览器并登录
|
||||
pw, context, page, _ = await _start_and_login(
|
||||
profile_dir, username, password, headless
|
||||
)
|
||||
|
||||
# 导航到发票页面
|
||||
if not await _navigate_to_invoice_page(page):
|
||||
# 步骤 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="导航到发票页面失败",
|
||||
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",
|
||||
)
|
||||
|
||||
# 提取发票信息
|
||||
result_data = await _extract_invoices(page, detail_mode)
|
||||
|
||||
|
||||
# 刷新 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(
|
||||
@@ -187,7 +228,7 @@ async def run_invoice_rpa(
|
||||
error=str(e),
|
||||
stage="unknown",
|
||||
)
|
||||
|
||||
|
||||
finally:
|
||||
# 关闭浏览器
|
||||
if pw and context:
|
||||
@@ -207,7 +248,7 @@ async def run_invoice_rpa_with_video(
|
||||
) -> InvoiceRpaResult:
|
||||
"""带录屏的发票RPA主流程。"""
|
||||
batch_id = uuid.uuid4().hex[:12]
|
||||
|
||||
|
||||
async with RpaVideoSession(
|
||||
skill_slug=skill_slug,
|
||||
skill_data_dir=skill_data_dir,
|
||||
@@ -216,7 +257,7 @@ async def run_invoice_rpa_with_video(
|
||||
closing_title=closing_title,
|
||||
) as video:
|
||||
video.add_step("准备启动浏览器")
|
||||
|
||||
|
||||
# 运行RPA
|
||||
result = await run_invoice_rpa(
|
||||
username=username,
|
||||
@@ -227,10 +268,10 @@ async def run_invoice_rpa_with_video(
|
||||
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
|
||||
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user