chore: auto release commit (2026-09-14 15:11:41)
All checks were successful
技能自动化发布 / release (push) Successful in 9s
All checks were successful
技能自动化发布 / release (push) Successful in 9s
This commit is contained in:
@@ -3,6 +3,15 @@
|
|||||||
本文件对应技能市场 **「更新日志」** Tab。
|
本文件对应技能市场 **「更新日志」** Tab。
|
||||||
发布 CI 会按当前 tag 版本提取匹配的 `##` 小节,写入该版本的 `changelog`。
|
发布 CI 会按当前 tag 版本提取匹配的 `##` 小节,写入该版本的 `changelog`。
|
||||||
|
|
||||||
|
## 1.0.2
|
||||||
|
|
||||||
|
- 新增RPA(机器人流程自动化)功能,支持从网站自动提取发票信息
|
||||||
|
- 新增CLI命令:`extract-invoices`、`test-rpa-connection`、`list-rpa-invoices`、`show-rpa-invoice`、`rpa-invoice-stats`
|
||||||
|
- 新增RPA数据存储功能,提取的发票数据自动保存到数据库
|
||||||
|
- 新增RPA数据服务模块,支持查询和管理RPA提取的数据
|
||||||
|
- 新增平台内使用指南,支持CLI、Action、对话三种使用方式
|
||||||
|
- 优化数据存储架构,新增`rpa_invoices`表存储RPA提取的数据
|
||||||
|
|
||||||
## 1.0.1
|
## 1.0.1
|
||||||
|
|
||||||
- 首个版本:票据合规性"秒级预审"上线
|
- 首个版本:票据合规性"秒级预审"上线
|
||||||
|
|||||||
370
PLATFORM_USAGE.md
Normal file
370
PLATFORM_USAGE.md
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
# 平台内使用RPA功能的完整指南
|
||||||
|
|
||||||
|
## 📋 概述
|
||||||
|
|
||||||
|
本指南详细说明如何将带RPA功能的verify-invoice-default技能发布到平台,并在平台内使用RPA功能。
|
||||||
|
|
||||||
|
## 🎯 平台内使用方式
|
||||||
|
|
||||||
|
### 1. **通过CLI命令使用**(推荐)
|
||||||
|
|
||||||
|
在平台内,用户可以通过以下CLI命令使用RPA功能:
|
||||||
|
|
||||||
|
#### **1.1 提取发票信息**
|
||||||
|
```bash
|
||||||
|
# 基本提取
|
||||||
|
python scripts/main.py extract-invoices \
|
||||||
|
--username "用户名" \
|
||||||
|
--password "密码" \
|
||||||
|
--profile-dir "浏览器配置目录"
|
||||||
|
|
||||||
|
# 详细信息提取
|
||||||
|
python scripts/main.py extract-invoices \
|
||||||
|
--username "用户名" \
|
||||||
|
--password "密码" \
|
||||||
|
--profile-dir "浏览器配置目录" \
|
||||||
|
--detail-mode
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **1.2 测试RPA连接**
|
||||||
|
```bash
|
||||||
|
python scripts/main.py test-rpa-connection \
|
||||||
|
--username "用户名" \
|
||||||
|
--password "密码" \
|
||||||
|
--profile-dir "浏览器配置目录"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **1.3 查询提取的数据**
|
||||||
|
```bash
|
||||||
|
# 查询所有提取记录
|
||||||
|
python scripts/main.py list-rpa-invoices
|
||||||
|
|
||||||
|
# 按批次ID查询
|
||||||
|
python scripts/main.py list-rpa-invoices --batch-id "批次ID"
|
||||||
|
|
||||||
|
# 按状态查询
|
||||||
|
python scripts/main.py list-rpa-invoices --status "extracted"
|
||||||
|
|
||||||
|
# 查看详细记录
|
||||||
|
python scripts/main.py show-rpa-invoice --record-id 1
|
||||||
|
|
||||||
|
# 获取统计信息
|
||||||
|
python scripts/main.py rpa-invoice-stats
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **通过Action调用**(平台集成)
|
||||||
|
|
||||||
|
在平台内,可以通过`run_skill_action`调用RPA功能:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 提取发票信息(异步)
|
||||||
|
run_skill_action --action extract-invoices \
|
||||||
|
--input '{"username": "用户名", "password": "密码", "profile_dir": "目录"}'
|
||||||
|
|
||||||
|
# 查询提取记录
|
||||||
|
run_skill_action --action list-rpa-invoices \
|
||||||
|
--input '{"limit": 10, "batch_id": "批次ID"}'
|
||||||
|
|
||||||
|
# 获取统计信息
|
||||||
|
run_skill_action --action rpa-invoice-stats
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **通过对话交互**(LLM集成)
|
||||||
|
|
||||||
|
用户可以通过自然语言与LLM交互来使用RPA功能:
|
||||||
|
|
||||||
|
**示例对话:**
|
||||||
|
```
|
||||||
|
用户:帮我从网站提取发票信息
|
||||||
|
LLM:好的,我需要一些信息来执行RPA操作:
|
||||||
|
1. 网站用户名
|
||||||
|
2. 网站密码
|
||||||
|
3. 浏览器配置目录(需要先通过account-manager获取)
|
||||||
|
|
||||||
|
请提供这些信息,我将为您提取发票信息。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 平台内配置步骤
|
||||||
|
|
||||||
|
### 步骤1:获取浏览器配置目录
|
||||||
|
|
||||||
|
在平台内,用户需要先通过account-manager获取浏览器配置目录:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 登录account-manager
|
||||||
|
python account-manager/scripts/main.py ensure-web --platform sandbox-jc2009
|
||||||
|
|
||||||
|
# 或者使用简化的命令
|
||||||
|
python account-manager/scripts/main.py pick-web --platform sandbox-jc2009 --ensure
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤2:配置环境变量
|
||||||
|
|
||||||
|
创建`.env`文件配置必要的环境变量:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# RPA配置
|
||||||
|
OPENCLAW_BROWSER_HEADLESS=0
|
||||||
|
OPENCLAW_RECORD_VIDEO=1
|
||||||
|
HUMAN_WAIT_TIMEOUT=120
|
||||||
|
|
||||||
|
# 日志配置
|
||||||
|
LOG_LEVEL=INFO
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤3:初始化数据库
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/main.py init-db
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 数据流架构
|
||||||
|
|
||||||
|
### 平台内数据流
|
||||||
|
|
||||||
|
```
|
||||||
|
用户请求 → LLM/CLI → RPA模块 → 浏览器自动化 → 网站数据提取
|
||||||
|
↓
|
||||||
|
数据存储 → SQLite数据库
|
||||||
|
↓
|
||||||
|
查询接口 → 数据管理界面
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数据存储位置
|
||||||
|
|
||||||
|
1. **SQLite数据库**:`{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/verify-invoice-default/data.db`
|
||||||
|
- `rpa_invoices`表:存储RPA提取的原始数据
|
||||||
|
- `invoice_records`表:存储预审后的发票记录
|
||||||
|
- `prereview_reports`表:存储预审报告
|
||||||
|
|
||||||
|
2. **录屏文件**:`{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/verify-invoice-default/videos/`
|
||||||
|
|
||||||
|
3. **日志文件**:`{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/verify-invoice-default/logs/`
|
||||||
|
|
||||||
|
## 🎮 平台内使用场景
|
||||||
|
|
||||||
|
### 场景1:批量发票提取
|
||||||
|
|
||||||
|
**用户需求**:从网站批量提取100张发票信息
|
||||||
|
|
||||||
|
**操作步骤**:
|
||||||
|
1. 用户通过CLI或对话发起请求
|
||||||
|
2. RPA模块自动登录网站
|
||||||
|
3. 提取发票列表信息
|
||||||
|
4. 保存到数据库
|
||||||
|
5. 返回提取结果
|
||||||
|
|
||||||
|
**命令示例**:
|
||||||
|
```bash
|
||||||
|
python scripts/main.py extract-invoices \
|
||||||
|
--username "15527406659" \
|
||||||
|
--password "qwer1234" \
|
||||||
|
--profile-dir "/path/to/profile"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景2:发票信息查询
|
||||||
|
|
||||||
|
**用户需求**:查看之前提取的发票信息
|
||||||
|
|
||||||
|
**操作步骤**:
|
||||||
|
1. 用户查询提取记录
|
||||||
|
2. 系统从数据库读取数据
|
||||||
|
3. 返回结构化结果
|
||||||
|
|
||||||
|
**命令示例**:
|
||||||
|
```bash
|
||||||
|
# 查询最近10条记录
|
||||||
|
python scripts/main.py list-rpa-invoices --limit 10
|
||||||
|
|
||||||
|
# 查看特定记录详情
|
||||||
|
python scripts/main.py show-rpa-invoice --record-id 5
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景3:统计分析
|
||||||
|
|
||||||
|
**用户需求**:查看RPA提取的统计信息
|
||||||
|
|
||||||
|
**操作步骤**:
|
||||||
|
1. 用户请求统计信息
|
||||||
|
2. 系统查询数据库统计
|
||||||
|
3. 返回统计结果
|
||||||
|
|
||||||
|
**命令示例**:
|
||||||
|
```bash
|
||||||
|
python scripts/main.py rpa-invoice-stats
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔐 安全考虑
|
||||||
|
|
||||||
|
### 1. 账号安全
|
||||||
|
|
||||||
|
- **密码加密**:建议在`.env`中配置密码,不要硬编码
|
||||||
|
- **权限控制**:通过account-manager管理账号权限
|
||||||
|
- **会话管理**:浏览器配置目录隔离不同用户
|
||||||
|
|
||||||
|
### 2. 数据安全
|
||||||
|
|
||||||
|
- **数据脱敏**:日志中自动脱敏敏感信息
|
||||||
|
- **访问控制**:通过平台权限系统控制数据访问
|
||||||
|
- **审计日志**:记录所有RPA操作
|
||||||
|
|
||||||
|
### 3. 操作安全
|
||||||
|
|
||||||
|
- **频率控制**:避免频繁请求,防止被网站限制
|
||||||
|
- **人工兜底**:验证码、人脸等必须等待人工完成
|
||||||
|
- **错误处理**:完善的异常捕获和错误报告
|
||||||
|
|
||||||
|
## 📈 平台集成优势
|
||||||
|
|
||||||
|
### 1. 统一管理
|
||||||
|
|
||||||
|
- **账号管理**:通过account-manager统一管理浏览器配置
|
||||||
|
- **权限控制**:平台级权限管理
|
||||||
|
- **日志聚合**:统一日志查看和分析
|
||||||
|
|
||||||
|
### 2. 数据共享
|
||||||
|
|
||||||
|
- **数据存储**:统一数据存储和备份
|
||||||
|
- **数据查询**:通过数据管理界面查询
|
||||||
|
- **数据导出**:支持多种格式导出
|
||||||
|
|
||||||
|
### 3. 监控运维
|
||||||
|
|
||||||
|
- **健康检查**:平台级健康检查
|
||||||
|
- **性能监控**:RPA操作性能监控
|
||||||
|
- **告警通知**:异常情况自动告警
|
||||||
|
|
||||||
|
## 🚀 发布到平台的步骤
|
||||||
|
|
||||||
|
### 步骤1:准备发布文件
|
||||||
|
|
||||||
|
确保以下文件已准备就绪:
|
||||||
|
|
||||||
|
```
|
||||||
|
verify-invoice-default/
|
||||||
|
├── SKILL.md # 技能说明
|
||||||
|
├── README.md # 用户说明
|
||||||
|
├── TUTORIAL.md # 使用教程
|
||||||
|
├── CHANGELOG.md # 更新日志
|
||||||
|
├── scripts/ # 脚本目录
|
||||||
|
│ ├── main.py # CLI入口
|
||||||
|
│ └── service/ # 服务模块
|
||||||
|
├── development/ # 开发文档
|
||||||
|
└── examples/ # 示例文件
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤2:验证功能
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 测试CLI命令
|
||||||
|
python scripts/main.py health
|
||||||
|
python scripts/main.py version
|
||||||
|
|
||||||
|
# 测试RPA功能
|
||||||
|
python scripts/main.py test-rpa-connection \
|
||||||
|
--username "测试用户" \
|
||||||
|
--password "测试密码" \
|
||||||
|
--profile-dir "测试目录"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤3:打包发布
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 使用release脚本
|
||||||
|
python tools/release.ps1
|
||||||
|
|
||||||
|
# 或者手动打包
|
||||||
|
tar -czf verify-invoice-default.tar.gz verify-invoice-default/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤4:上传到平台
|
||||||
|
|
||||||
|
1. 登录平台管理后台
|
||||||
|
2. 上传技能包
|
||||||
|
3. 配置技能元数据
|
||||||
|
4. 发布技能
|
||||||
|
|
||||||
|
## 📝 平台内使用示例
|
||||||
|
|
||||||
|
### 示例1:通过CLI提取发票
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 获取浏览器配置
|
||||||
|
python account-manager/scripts/main.py ensure-web --platform sandbox-jc2009
|
||||||
|
|
||||||
|
# 2. 提取发票信息
|
||||||
|
python scripts/main.py extract-invoices \
|
||||||
|
--username "15527406659" \
|
||||||
|
--password "qwer1234" \
|
||||||
|
--profile-dir "/path/to/profile"
|
||||||
|
|
||||||
|
# 3. 查看提取结果
|
||||||
|
python scripts/main.py list-rpa-invoices --limit 5
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例2:通过对话交互
|
||||||
|
|
||||||
|
```
|
||||||
|
用户:我想提取网站上的发票信息
|
||||||
|
LLM:好的,我来帮您提取发票信息。请提供:
|
||||||
|
1. 网站用户名:15527406659
|
||||||
|
2. 网站密码:qwer1234
|
||||||
|
3. 浏览器配置目录:/path/to/profile
|
||||||
|
|
||||||
|
LLM:正在执行RPA提取...
|
||||||
|
LLM:成功提取了10张发票信息,已保存到数据库。
|
||||||
|
|
||||||
|
用户:查看提取的发票详情
|
||||||
|
LLM:为您查询到最近5条提取记录:
|
||||||
|
1. 批次ID: abc123, 提取时间: 2026-09-14 10:30:00, 状态: 已提取
|
||||||
|
2. 批次ID: def456, 提取时间: 2026-09-14 11:00:00, 状态: 已提取
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 故障排除
|
||||||
|
|
||||||
|
### 问题1:浏览器启动失败
|
||||||
|
|
||||||
|
**错误**:`ERROR:MISSING_BROWSER 浏览器启动失败`
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
1. 检查是否已安装Chrome或Edge浏览器
|
||||||
|
2. 确认`profile_dir`是否有效
|
||||||
|
3. 检查是否有其他浏览器进程占用配置目录
|
||||||
|
|
||||||
|
### 问题2:登录失败
|
||||||
|
|
||||||
|
**错误**:`ERROR:LOGIN_TIMEOUT 登录失败或超时`
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
1. 检查用户名和密码是否正确
|
||||||
|
2. 确认网络连接是否正常
|
||||||
|
3. 检查是否有验证码需要人工处理
|
||||||
|
|
||||||
|
### 问题3:数据保存失败
|
||||||
|
|
||||||
|
**错误**:数据库写入失败
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
1. 检查数据库文件权限
|
||||||
|
2. 确认数据库表是否已创建
|
||||||
|
3. 检查磁盘空间是否充足
|
||||||
|
|
||||||
|
## 📞 技术支持
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
1. **如何获取浏览器配置目录?**
|
||||||
|
- 使用`account-manager`工具:`python account-manager/scripts/main.py ensure-web --platform sandbox-jc2009`
|
||||||
|
|
||||||
|
2. **如何查看提取的发票数据?**
|
||||||
|
- 使用命令:`python scripts/main.py list-rpa-invoices`
|
||||||
|
|
||||||
|
3. **如何导出提取的数据?**
|
||||||
|
- 使用命令:`python scripts/main.py show-rpa-invoice --record-id 1`,然后复制JSON输出
|
||||||
|
|
||||||
|
### 联系方式
|
||||||
|
|
||||||
|
- 技术支持:support@jiangchang.com
|
||||||
|
- 文档地址:https://docs.jiangchang.com/verify-invoice-default
|
||||||
|
- 问题反馈:https://github.com/jiangchang/verify-invoice-default/issues
|
||||||
212
RPA_README.md
Normal file
212
RPA_README.md
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
# 发票RPA功能
|
||||||
|
|
||||||
|
## 简介
|
||||||
|
|
||||||
|
本功能为verify-invoice-default技能添加了RPA(机器人流程自动化)能力,可以自动从网站提取发票信息,用于后续的票据合规预审。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- 🤖 **自动登录**:支持用户名密码自动登录
|
||||||
|
- 🌐 **页面导航**:自动导航到发票预检页面
|
||||||
|
- 📊 **信息提取**:提取发票列表和详细信息
|
||||||
|
- 🎭 **拟人操作**:模拟真实用户操作,避免被检测
|
||||||
|
- ⚠️ **错误处理**:处理登录超时、验证码等异常情况
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 1. 获取浏览器配置目录
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 确保已安装account-manager
|
||||||
|
cd account-manager
|
||||||
|
python scripts/main.py ensure-web --platform sandbox-jc2009
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 测试连接
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd verify-invoice-default/verify-invoice-default
|
||||||
|
python scripts/main.py test-rpa-connection \
|
||||||
|
--username "15527406659" \
|
||||||
|
--password "qwer1234" \
|
||||||
|
--profile-dir "从account-manager获取的profile_dir"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 提取发票信息
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 基本提取
|
||||||
|
python scripts/main.py extract-invoices \
|
||||||
|
--username "15527406659" \
|
||||||
|
--password "qwer1234" \
|
||||||
|
--profile-dir "从account-manager获取的profile_dir"
|
||||||
|
|
||||||
|
# 详细信息提取
|
||||||
|
python scripts/main.py extract-invoices \
|
||||||
|
--username "15527406659" \
|
||||||
|
--password "qwer1234" \
|
||||||
|
--profile-dir "从account-manager获取的profile_dir" \
|
||||||
|
--detail-mode
|
||||||
|
```
|
||||||
|
|
||||||
|
## 命令行参数
|
||||||
|
|
||||||
|
### extract-invoices
|
||||||
|
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `--username` | 是 | 网站用户名 |
|
||||||
|
| `--password` | 是 | 网站密码 |
|
||||||
|
| `--profile-dir` | 是 | 浏览器配置目录 |
|
||||||
|
| `--headless` | 否 | 无头模式(生产不建议) |
|
||||||
|
| `--detail-mode` | 否 | 提取详细信息 |
|
||||||
|
|
||||||
|
### test-rpa-connection
|
||||||
|
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `--username` | 是 | 网站用户名 |
|
||||||
|
| `--password` | 是 | 网站密码 |
|
||||||
|
| `--profile-dir` | 是 | 浏览器配置目录 |
|
||||||
|
|
||||||
|
## 输出格式
|
||||||
|
|
||||||
|
### 成功输出
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"total_count": 10,
|
||||||
|
"invoices": [
|
||||||
|
{
|
||||||
|
"row_index": 0,
|
||||||
|
"row_text": "发票信息...",
|
||||||
|
"invoice_number": "12345678",
|
||||||
|
"amount": "100.00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extraction_complete": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 失败输出
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "错误信息",
|
||||||
|
"stage": "错误发生阶段"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
verify-invoice-default/verify-invoice-default/
|
||||||
|
├── scripts/
|
||||||
|
│ ├── service/
|
||||||
|
│ │ ├── rpa/
|
||||||
|
│ │ │ ├── __init__.py # RPA模块初始化
|
||||||
|
│ │ │ ├── browser_session.py # 浏览器会话管理
|
||||||
|
│ │ │ ├── login_handler.py # 登录处理
|
||||||
|
│ │ │ ├── invoice_extractor.py # 发票信息提取
|
||||||
|
│ │ │ └── invoice_rpa.py # RPA主流程
|
||||||
|
│ │ └── rpa_invoice_service.py # RPA数据服务
|
||||||
|
│ ├── db/
|
||||||
|
│ │ ├── connection.py # 数据库连接
|
||||||
|
│ │ └── rpa_invoice_schema.py # RPA数据表结构
|
||||||
|
│ └── cli/
|
||||||
|
│ └── app.py # CLI入口(已更新)
|
||||||
|
├── test_rpa.py # 测试脚本
|
||||||
|
├── demo_rpa.py # 演示脚本
|
||||||
|
├── RPA_README.md # 本文件
|
||||||
|
├── RPA_USAGE.md # 详细使用说明
|
||||||
|
└── PLATFORM_USAGE.md # 平台内使用指南
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### 1. 浏览器启动失败
|
||||||
|
|
||||||
|
**错误**:`ERROR:MISSING_BROWSER 浏览器启动失败`
|
||||||
|
|
||||||
|
**解决**:
|
||||||
|
- 确保已安装Chrome或Edge浏览器
|
||||||
|
- 检查`profile_dir`是否有效
|
||||||
|
- 确保没有其他浏览器进程占用配置目录
|
||||||
|
|
||||||
|
### 2. 登录失败
|
||||||
|
|
||||||
|
**错误**:`ERROR:LOGIN_TIMEOUT 登录失败或超时`
|
||||||
|
|
||||||
|
**解决**:
|
||||||
|
- 检查用户名和密码是否正确
|
||||||
|
- 确保网络连接正常
|
||||||
|
- 检查是否有验证码需要人工处理
|
||||||
|
|
||||||
|
### 3. 页面加载失败
|
||||||
|
|
||||||
|
**错误**:`导航到发票页面失败`
|
||||||
|
|
||||||
|
**解决**:
|
||||||
|
- 检查网络连接
|
||||||
|
- 确保已登录
|
||||||
|
- 检查目标URL是否正确
|
||||||
|
|
||||||
|
## 技术细节
|
||||||
|
|
||||||
|
### 架构设计
|
||||||
|
|
||||||
|
RPA功能采用分层架构设计:
|
||||||
|
|
||||||
|
1. **浏览器会话层**:管理浏览器的启动、配置和关闭
|
||||||
|
2. **登录处理层**:处理网站登录流程
|
||||||
|
3. **信息提取层**:从页面提取发票信息
|
||||||
|
4. **主流程层**:协调各层完成RPA任务
|
||||||
|
|
||||||
|
### 安全考虑
|
||||||
|
|
||||||
|
1. **合规性**:仅用于授权范围内的正常界面操作
|
||||||
|
2. **频率控制**:避免频繁请求,防止被网站限制
|
||||||
|
3. **人工兜底**:验证码、人脸等必须等待人工完成
|
||||||
|
4. **数据安全**:不要在日志中暴露敏感信息
|
||||||
|
|
||||||
|
### 扩展性
|
||||||
|
|
||||||
|
RPA功能设计为可扩展的:
|
||||||
|
|
||||||
|
1. **支持新网站**:只需修改URL和选择器
|
||||||
|
2. **支持新字段**:在提取器中添加新的选择器
|
||||||
|
3. **支持新操作**:可以添加点击、填写等操作
|
||||||
|
|
||||||
|
## 开发指南
|
||||||
|
|
||||||
|
### 添加新的提取字段
|
||||||
|
|
||||||
|
1. 在`invoice_extractor.py`的`INVOICE_SELECTORS`中添加选择器
|
||||||
|
2. 在`_extract_invoice_from_row`函数中添加提取逻辑
|
||||||
|
3. 更新`format_invoice_data`函数的输出格式
|
||||||
|
|
||||||
|
### 修改登录流程
|
||||||
|
|
||||||
|
1. 在`login_handler.py`的`LOGIN_SELECTORS`中更新选择器
|
||||||
|
2. 修改`_try_auto_login`函数的登录逻辑
|
||||||
|
3. 更新`_is_logged_in`函数的检测逻辑
|
||||||
|
|
||||||
|
### 添加新的RPA功能
|
||||||
|
|
||||||
|
1. 在`service/rpa/`目录下创建新模块
|
||||||
|
2. 在`__init__.py`中导出新功能
|
||||||
|
3. 在`task_service.py`中添加对应的命令函数
|
||||||
|
4. 在`cli/app.py`中添加命令行参数
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
### v1.0.0 (2026-09-14)
|
||||||
|
- 初始版本
|
||||||
|
- 实现基本登录和页面导航
|
||||||
|
- 实现发票信息提取
|
||||||
|
- 集成到现有CLI系统
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
本项目遵循与verify-invoice-default技能相同的许可证。
|
||||||
217
RPA_USAGE.md
Normal file
217
RPA_USAGE.md
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
# 发票RPA功能使用说明
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本功能为verify-invoice-default技能添加了RPA(机器人流程自动化)能力,可以自动从`https://sandbox.jc2009.com/industries/campus/smartcampus/app/finance/invoice-precheck`网站提取发票信息。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
1. **自动登录**:支持用户名密码自动登录
|
||||||
|
2. **页面导航**:自动导航到发票预检页面
|
||||||
|
3. **信息提取**:提取发票列表和详细信息
|
||||||
|
4. **拟人操作**:模拟真实用户操作,避免被检测
|
||||||
|
5. **错误处理**:处理登录超时、验证码等异常情况
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
1. **浏览器配置目录**:需要通过account-manager获取`profile_dir`
|
||||||
|
2. **Python环境**:需要Python 3.10+环境
|
||||||
|
3. **依赖包**:需要`jiangchang-platform-kit>=1.2.11`
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 1. 获取浏览器配置目录
|
||||||
|
|
||||||
|
首先,需要通过account-manager获取浏览器配置目录:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 登录account-manager
|
||||||
|
python account-manager/scripts/main.py ensure-web --platform sandbox-jc2009
|
||||||
|
|
||||||
|
# 或者使用简化的命令
|
||||||
|
python account-manager/scripts/main.py pick-web --platform sandbox-jc2009 --ensure
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 测试RPA连接
|
||||||
|
|
||||||
|
在使用前,建议先测试连接和登录功能:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd verify-invoice-default/verify-invoice-default
|
||||||
|
python scripts/main.py test-rpa-connection \
|
||||||
|
--username "15527406659" \
|
||||||
|
--password "qwer1234" \
|
||||||
|
--profile-dir "从account-manager获取的profile_dir"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 提取发票信息
|
||||||
|
|
||||||
|
#### 基本提取(列表模式)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/main.py extract-invoices \
|
||||||
|
--username "15527406659" \
|
||||||
|
--password "qwer1234" \
|
||||||
|
--profile-dir "从account-manager获取的profile_dir"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 详细信息提取
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/main.py extract-invoices \
|
||||||
|
--username "15527406659" \
|
||||||
|
--password "qwer1234" \
|
||||||
|
--profile-dir "从account-manager获取的profile_dir" \
|
||||||
|
--detail-mode
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 无头模式(不推荐生产使用)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/main.py extract-invoices \
|
||||||
|
--username "15527406659" \
|
||||||
|
--password "qwer1234" \
|
||||||
|
--profile-dir "从account-manager获取的profile_dir" \
|
||||||
|
--headless
|
||||||
|
```
|
||||||
|
|
||||||
|
## 命令参数说明
|
||||||
|
|
||||||
|
### extract-invoices 命令
|
||||||
|
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `--username` | 是 | 网站用户名 |
|
||||||
|
| `--password` | 是 | 网站密码 |
|
||||||
|
| `--profile-dir` | 是 | 浏览器配置目录 |
|
||||||
|
| `--headless` | 否 | 无头模式(生产不建议) |
|
||||||
|
| `--detail-mode` | 否 | 提取详细信息 |
|
||||||
|
|
||||||
|
### test-rpa-connection 命令
|
||||||
|
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `--username` | 是 | 网站用户名 |
|
||||||
|
| `--password` | 是 | 网站密码 |
|
||||||
|
| `--profile-dir` | 是 | 浏览器配置目录 |
|
||||||
|
|
||||||
|
## 输出格式
|
||||||
|
|
||||||
|
### 成功输出
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"total_count": 10,
|
||||||
|
"invoices": [
|
||||||
|
{
|
||||||
|
"row_index": 0,
|
||||||
|
"row_text": "发票信息...",
|
||||||
|
"invoice_number": "12345678",
|
||||||
|
"amount": "100.00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extraction_complete": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 失败输出
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "错误信息",
|
||||||
|
"stage": "错误发生阶段"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### 1. 浏览器启动失败
|
||||||
|
|
||||||
|
**错误信息**:`ERROR:MISSING_BROWSER 浏览器启动失败`
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
- 确保已安装Chrome或Edge浏览器
|
||||||
|
- 检查`profile_dir`是否有效
|
||||||
|
- 确保没有其他浏览器进程占用配置目录
|
||||||
|
|
||||||
|
### 2. 登录失败
|
||||||
|
|
||||||
|
**错误信息**:`ERROR:LOGIN_TIMEOUT 登录失败或超时`
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
- 检查用户名和密码是否正确
|
||||||
|
- 确保网络连接正常
|
||||||
|
- 检查是否有验证码需要人工处理
|
||||||
|
|
||||||
|
### 3. 页面加载失败
|
||||||
|
|
||||||
|
**错误信息**:`导航到发票页面失败`
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
- 检查网络连接
|
||||||
|
- 确保已登录
|
||||||
|
- 检查目标URL是否正确
|
||||||
|
|
||||||
|
## 技术实现
|
||||||
|
|
||||||
|
### 架构设计
|
||||||
|
|
||||||
|
```
|
||||||
|
invoice_rpa.py # RPA主流程协调
|
||||||
|
├── browser_session.py # 浏览器会话管理
|
||||||
|
├── login_handler.py # 登录处理
|
||||||
|
└── invoice_extractor.py # 发票信息提取
|
||||||
|
```
|
||||||
|
|
||||||
|
### 核心流程
|
||||||
|
|
||||||
|
1. **启动浏览器**:使用Playwright启动持久化浏览器上下文
|
||||||
|
2. **登录网站**:自动输入用户名密码,处理验证码
|
||||||
|
3. **导航页面**:跳转到发票预检页面
|
||||||
|
4. **提取信息**:解析页面DOM,提取发票数据
|
||||||
|
5. **返回结果**:格式化输出提取的发票信息
|
||||||
|
|
||||||
|
### 安全特性
|
||||||
|
|
||||||
|
1. **拟人操作**:模拟真实用户操作节奏
|
||||||
|
2. **错误处理**:完善的异常捕获和错误报告
|
||||||
|
3. **资源清理**:确保浏览器正确关闭
|
||||||
|
4. **日志记录**:详细的操作日志
|
||||||
|
|
||||||
|
## 开发说明
|
||||||
|
|
||||||
|
### 添加新的提取字段
|
||||||
|
|
||||||
|
1. 在`invoice_extractor.py`的`INVOICE_SELECTORS`中添加选择器
|
||||||
|
2. 在`_extract_invoice_from_row`函数中添加提取逻辑
|
||||||
|
3. 更新`format_invoice_data`函数的输出格式
|
||||||
|
|
||||||
|
### 修改登录流程
|
||||||
|
|
||||||
|
1. 在`login_handler.py`的`LOGIN_SELECTORS`中更新选择器
|
||||||
|
2. 修改`_try_auto_login`函数的登录逻辑
|
||||||
|
3. 更新`_is_logged_in`函数的检测逻辑
|
||||||
|
|
||||||
|
### 添加新的RPA功能
|
||||||
|
|
||||||
|
1. 在`service/rpa/`目录下创建新模块
|
||||||
|
2. 在`__init__.py`中导出新功能
|
||||||
|
3. 在`task_service.py`中添加对应的命令函数
|
||||||
|
4. 在`cli/app.py`中添加命令行参数
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **合规性**:仅用于授权范围内的正常界面操作
|
||||||
|
2. **频率控制**:避免频繁请求,防止被网站限制
|
||||||
|
3. **人工兜底**:验证码、人脸等必须等待人工完成
|
||||||
|
4. **数据安全**:不要在日志中暴露敏感信息
|
||||||
|
5. **资源管理**:确保浏览器会话正确关闭
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
### v1.0.0 (2026-09-14)
|
||||||
|
- 初始版本
|
||||||
|
- 实现基本登录和页面导航
|
||||||
|
- 实现发票信息提取
|
||||||
|
- 集成到现有CLI系统
|
||||||
2
SKILL.md
2
SKILL.md
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: 票据合规预审助手
|
name: 票据合规预审助手
|
||||||
description: "高校报销场景的发票合规性秒级预审:核对字段完整性、购买方税号白名单、开票日期与出差行程逻辑、住宿交通经费标准,对连号、同商家集中开票、节假日开票等风险点红黄灯预警,生成《初审意见单》。"
|
description: "高校报销场景的发票合规性秒级预审:核对字段完整性、购买方税号白名单、开票日期与出差行程逻辑、住宿交通经费标准,对连号、同商家集中开票、节假日开票等风险点红黄灯预警,生成《初审意见单》。"
|
||||||
version: 1.0.1
|
version: 1.0.2
|
||||||
author: 深圳匠厂科技有限公司
|
author: 深圳匠厂科技有限公司
|
||||||
metadata:
|
metadata:
|
||||||
openclaw:
|
openclaw:
|
||||||
|
|||||||
95
demo_rpa.py
Normal file
95
demo_rpa.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""发票RPA功能演示脚本。
|
||||||
|
|
||||||
|
使用方法:
|
||||||
|
1. 首先获取浏览器配置目录:
|
||||||
|
python account-manager/scripts/main.py ensure-web --platform sandbox-jc2009
|
||||||
|
|
||||||
|
2. 运行演示:
|
||||||
|
python demo_rpa.py --profile-dir "从account-manager获取的profile_dir"
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 添加scripts目录到Python路径
|
||||||
|
scripts_dir = os.path.join(os.path.dirname(__file__), "scripts")
|
||||||
|
if scripts_dir not in sys.path:
|
||||||
|
sys.path.insert(0, scripts_dir)
|
||||||
|
|
||||||
|
from service.rpa import run_invoice_rpa
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""主函数。"""
|
||||||
|
parser = argparse.ArgumentParser(description="发票RPA功能演示")
|
||||||
|
parser.add_argument("--profile-dir", required=True, help="浏览器配置目录")
|
||||||
|
parser.add_argument("--username", default="15527406659", help="用户名")
|
||||||
|
parser.add_argument("--password", default="qwer1234", help="密码")
|
||||||
|
parser.add_argument("--headless", action="store_true", help="无头模式")
|
||||||
|
parser.add_argument("--detail", action="store_true", help="提取详细信息")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("发票RPA功能演示")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"目标网站: https://sandbox.jc2009.com/industries/campus/smartcampus/app/finance/invoice-precheck")
|
||||||
|
print(f"用户名: {args.username[:3]}***")
|
||||||
|
print(f"浏览器配置: {args.profile_dir}")
|
||||||
|
print(f"模式: {'无头' if args.headless else '有头'}")
|
||||||
|
print(f"提取模式: {'详细' if args.detail else '列表'}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
try:
|
||||||
|
print("\n正在启动浏览器并登录...")
|
||||||
|
result = await run_invoice_rpa(
|
||||||
|
username=args.username,
|
||||||
|
password=args.password,
|
||||||
|
profile_dir=args.profile_dir,
|
||||||
|
headless=args.headless,
|
||||||
|
detail_mode=args.detail,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
print("\n✅ 发票信息提取成功!")
|
||||||
|
print(f"提取到 {result.data.get('total_count', 0)} 张发票信息")
|
||||||
|
|
||||||
|
# 显示前3条发票信息
|
||||||
|
invoices = result.data.get('invoices', [])
|
||||||
|
if invoices:
|
||||||
|
print("\n前3条发票信息:")
|
||||||
|
for i, invoice in enumerate(invoices[:3]):
|
||||||
|
print(f"\n发票 {i+1}:")
|
||||||
|
print(f" 行文本: {invoice.get('row_text', 'N/A')[:100]}...")
|
||||||
|
if 'invoice_number' in invoice:
|
||||||
|
print(f" 发票号码: {invoice['invoice_number']}")
|
||||||
|
if 'amount' in invoice:
|
||||||
|
print(f" 金额: {invoice['amount']}")
|
||||||
|
|
||||||
|
# 保存完整结果到文件
|
||||||
|
output_file = "invoice_extraction_result.json"
|
||||||
|
import json
|
||||||
|
with open(output_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(result.data, f, ensure_ascii=False, indent=2)
|
||||||
|
print(f"\n完整结果已保存到: {output_file}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"\n❌ 发票信息提取失败!")
|
||||||
|
print(f"错误信息: {result.error}")
|
||||||
|
print(f"错误阶段: {result.stage}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ 运行异常: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("演示完成")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -13,9 +13,11 @@ from typing import List, Optional
|
|||||||
from service.task_service import (
|
from service.task_service import (
|
||||||
cmd_add_whitelist,
|
cmd_add_whitelist,
|
||||||
cmd_config_path,
|
cmd_config_path,
|
||||||
|
cmd_extract_invoices,
|
||||||
cmd_health,
|
cmd_health,
|
||||||
cmd_init_db,
|
cmd_init_db,
|
||||||
cmd_list_holidays,
|
cmd_list_holidays,
|
||||||
|
cmd_list_rpa_invoices,
|
||||||
cmd_list_reports,
|
cmd_list_reports,
|
||||||
cmd_list_whitelist,
|
cmd_list_whitelist,
|
||||||
cmd_mark_verified,
|
cmd_mark_verified,
|
||||||
@@ -23,10 +25,13 @@ from service.task_service import (
|
|||||||
cmd_prereview_batch_input,
|
cmd_prereview_batch_input,
|
||||||
cmd_remove_whitelist,
|
cmd_remove_whitelist,
|
||||||
cmd_render_html,
|
cmd_render_html,
|
||||||
|
cmd_rpa_invoice_stats,
|
||||||
cmd_run,
|
cmd_run,
|
||||||
cmd_set_standard,
|
cmd_set_standard,
|
||||||
|
cmd_show_rpa_invoice,
|
||||||
cmd_show_report,
|
cmd_show_report,
|
||||||
cmd_stats,
|
cmd_stats,
|
||||||
|
cmd_test_rpa_connection,
|
||||||
cmd_version,
|
cmd_version,
|
||||||
cmd_view_standards,
|
cmd_view_standards,
|
||||||
)
|
)
|
||||||
@@ -134,6 +139,41 @@ def _handle_list_holidays(args: argparse.Namespace) -> int:
|
|||||||
return cmd_list_holidays(year=_cli_str_or_none(args.year))
|
return cmd_list_holidays(year=_cli_str_or_none(args.year))
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_extract_invoices(args: argparse.Namespace) -> int:
|
||||||
|
return cmd_extract_invoices(
|
||||||
|
username=str(args.username or "").strip(),
|
||||||
|
password=str(args.password or "").strip(),
|
||||||
|
profile_dir=str(args.profile_dir or "").strip(),
|
||||||
|
headless=bool(args.headless),
|
||||||
|
detail_mode=bool(args.detail_mode),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_test_rpa_connection(args: argparse.Namespace) -> int:
|
||||||
|
return cmd_test_rpa_connection(
|
||||||
|
username=str(args.username or "").strip(),
|
||||||
|
password=str(args.password or "").strip(),
|
||||||
|
profile_dir=str(args.profile_dir or "").strip(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_list_rpa_invoices(args: argparse.Namespace) -> int:
|
||||||
|
return cmd_list_rpa_invoices(
|
||||||
|
limit=int(args.limit or 20),
|
||||||
|
batch_id=_cli_str_or_none(args.batch_id),
|
||||||
|
username=_cli_str_or_none(args.username),
|
||||||
|
status=_cli_str_or_none(args.status),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_show_rpa_invoice(args: argparse.Namespace) -> int:
|
||||||
|
return cmd_show_rpa_invoice(record_id=int(args.record_id))
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_rpa_invoice_stats(args: argparse.Namespace) -> int:
|
||||||
|
return cmd_rpa_invoice_stats()
|
||||||
|
|
||||||
|
|
||||||
def _print_full_usage() -> None:
|
def _print_full_usage() -> None:
|
||||||
print("票据合规预审助手(main.py)可用命令:")
|
print("票据合规预审助手(main.py)可用命令:")
|
||||||
print(" python main.py prereview --invoice-json <file|-> [--purpose 文本] [--project-no 编号]")
|
print(" python main.py prereview --invoice-json <file|-> [--purpose 文本] [--project-no 编号]")
|
||||||
@@ -152,6 +192,9 @@ def _print_full_usage() -> None:
|
|||||||
print(" python main.py list-holidays [--year YYYY]")
|
print(" python main.py list-holidays [--year YYYY]")
|
||||||
print(" python main.py stats")
|
print(" python main.py stats")
|
||||||
print(" python main.py run [target [input_id]] [-t target] [-i input_id]")
|
print(" python main.py run [target [input_id]] [-t target] [-i input_id]")
|
||||||
|
print(" python main.py extract-invoices --username U --password P --profile-dir DIR")
|
||||||
|
print(" [--headless] [--detail-mode]")
|
||||||
|
print(" python main.py test-rpa-connection --username U --password P --profile-dir DIR")
|
||||||
print(" python main.py health")
|
print(" python main.py health")
|
||||||
print(" python main.py config-path")
|
print(" python main.py config-path")
|
||||||
print(" python main.py version")
|
print(" python main.py version")
|
||||||
@@ -244,6 +287,36 @@ def build_parser() -> ZhArgumentParser:
|
|||||||
sp = sub.add_parser("stats", help="预审统计")
|
sp = sub.add_parser("stats", help="预审统计")
|
||||||
sp.set_defaults(handler=lambda _a: cmd_stats())
|
sp.set_defaults(handler=lambda _a: cmd_stats())
|
||||||
|
|
||||||
|
# RPA commands
|
||||||
|
sp = sub.add_parser("extract-invoices", help="从网站提取发票信息")
|
||||||
|
sp.add_argument("--username", required=True, metavar="用户名")
|
||||||
|
sp.add_argument("--password", required=True, metavar="密码")
|
||||||
|
sp.add_argument("--profile-dir", required=True, metavar="浏览器配置目录", dest="profile_dir")
|
||||||
|
sp.add_argument("--headless", action="store_true", help="无头模式(生产不建议)")
|
||||||
|
sp.add_argument("--detail-mode", action="store_true", help="提取详细信息", dest="detail_mode")
|
||||||
|
sp.set_defaults(handler=_handle_extract_invoices)
|
||||||
|
|
||||||
|
sp = sub.add_parser("test-rpa-connection", help="测试RPA连接和登录")
|
||||||
|
sp.add_argument("--username", required=True, metavar="用户名")
|
||||||
|
sp.add_argument("--password", required=True, metavar="密码")
|
||||||
|
sp.add_argument("--profile-dir", required=True, metavar="浏览器配置目录", dest="profile_dir")
|
||||||
|
sp.set_defaults(handler=_handle_test_rpa_connection)
|
||||||
|
|
||||||
|
# RPA数据查询命令
|
||||||
|
sp = sub.add_parser("list-rpa-invoices", help="查询RPA提取的发票数据")
|
||||||
|
sp.add_argument("--limit", default=20, type=int, metavar="条数")
|
||||||
|
sp.add_argument("--batch-id", default=None, metavar="批次ID", dest="batch_id")
|
||||||
|
sp.add_argument("--username", default=None, metavar="用户名")
|
||||||
|
sp.add_argument("--status", default=None, metavar="状态")
|
||||||
|
sp.set_defaults(handler=_handle_list_rpa_invoices)
|
||||||
|
|
||||||
|
sp = sub.add_parser("show-rpa-invoice", help="查看RPA提取的发票详情")
|
||||||
|
sp.add_argument("--record-id", required=True, type=int, metavar="记录ID", dest="record_id")
|
||||||
|
sp.set_defaults(handler=_handle_show_rpa_invoice)
|
||||||
|
|
||||||
|
sp = sub.add_parser("rpa-invoice-stats", help="获取RPA提取统计信息")
|
||||||
|
sp.set_defaults(handler=_handle_rpa_invoice_stats)
|
||||||
|
|
||||||
sp = sub.add_parser("run", help="任务执行命令骨架")
|
sp = sub.add_parser("run", help="任务执行命令骨架")
|
||||||
sp.add_argument("--target", "-t", default=None, metavar="目标")
|
sp.add_argument("--target", "-t", default=None, metavar="目标")
|
||||||
sp.add_argument("--input-id", "-i", default=None, metavar="输入id", dest="input_id")
|
sp.add_argument("--input-id", "-i", default=None, metavar="输入id", dest="input_id")
|
||||||
|
|||||||
103
scripts/db/rpa_invoice_schema.py
Normal file
103
scripts/db/rpa_invoice_schema.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
"""RPA发票数据表结构定义。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
# RPA发票数据表DDL
|
||||||
|
RPA_INVOICES_DDL = """
|
||||||
|
CREATE TABLE IF NOT EXISTS rpa_invoices (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
batch_id TEXT NOT NULL,
|
||||||
|
source_url TEXT NOT NULL,
|
||||||
|
extraction_time INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||||
|
username TEXT,
|
||||||
|
invoice_data_json TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'extracted',
|
||||||
|
error_message TEXT,
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
RPA_INVOICES_INDEX = """
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_rpa_invoices_batch
|
||||||
|
ON rpa_invoices(batch_id)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# RPA发票数据表列定义
|
||||||
|
RPA_INVOICES_COLUMNS = (
|
||||||
|
{
|
||||||
|
"column_name": "id",
|
||||||
|
"display_name": "编号",
|
||||||
|
"description": "主键",
|
||||||
|
"visible": 1,
|
||||||
|
"searchable": 1,
|
||||||
|
"editable": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"column_name": "batch_id",
|
||||||
|
"display_name": "批次ID",
|
||||||
|
"description": "RPA提取批次的唯一标识",
|
||||||
|
"visible": 1,
|
||||||
|
"searchable": 1,
|
||||||
|
"editable": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"column_name": "source_url",
|
||||||
|
"display_name": "来源网址",
|
||||||
|
"description": "RPA操作的目标网站URL",
|
||||||
|
"visible": 1,
|
||||||
|
"searchable": 1,
|
||||||
|
"editable": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"column_name": "extraction_time",
|
||||||
|
"display_name": "提取时间",
|
||||||
|
"description": "RPA提取完成的时间(Unix秒级时间戳)",
|
||||||
|
"visible": 1,
|
||||||
|
"searchable": 0,
|
||||||
|
"editable": 0,
|
||||||
|
"display_type": "datetime_unix_seconds",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"column_name": "username",
|
||||||
|
"display_name": "操作用户",
|
||||||
|
"description": "执行RPA操作的用户",
|
||||||
|
"visible": 1,
|
||||||
|
"searchable": 1,
|
||||||
|
"editable": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"column_name": "invoice_data_json",
|
||||||
|
"display_name": "发票数据",
|
||||||
|
"description": "提取的发票数据JSON(完整数据)",
|
||||||
|
"visible": 0,
|
||||||
|
"searchable": 0,
|
||||||
|
"editable": 0,
|
||||||
|
"display_type": "textarea",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"column_name": "status",
|
||||||
|
"display_name": "状态",
|
||||||
|
"description": "提取状态:extracted/processed/error",
|
||||||
|
"visible": 1,
|
||||||
|
"searchable": 1,
|
||||||
|
"editable": 0,
|
||||||
|
"options_json": '[{"value":"extracted","label":"已提取"},{"value":"processed","label":"已处理"},{"value":"error","label":"错误"}]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"column_name": "error_message",
|
||||||
|
"display_name": "错误信息",
|
||||||
|
"description": "RPA执行过程中的错误信息",
|
||||||
|
"visible": 1,
|
||||||
|
"searchable": 1,
|
||||||
|
"editable": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"column_name": "created_at",
|
||||||
|
"display_name": "创建时间",
|
||||||
|
"description": "记录创建时间(Unix秒级时间戳)",
|
||||||
|
"visible": 1,
|
||||||
|
"searchable": 0,
|
||||||
|
"editable": 0,
|
||||||
|
"display_type": "datetime_unix_seconds",
|
||||||
|
},
|
||||||
|
)
|
||||||
18
scripts/service/rpa/__init__.py
Normal file
18
scripts/service/rpa/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
"""RPA模块:提供发票信息提取的自动化功能。"""
|
||||||
|
|
||||||
|
from .browser_session import close_browser_context, start_browser_session
|
||||||
|
from .invoice_extractor import extract_invoice_detail, extract_invoice_list, format_invoice_data
|
||||||
|
from .invoice_rpa import InvoiceRpaResult, run_invoice_rpa, run_invoice_rpa_with_video
|
||||||
|
from .login_handler import login_if_needed
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"start_browser_session",
|
||||||
|
"close_browser_context",
|
||||||
|
"login_if_needed",
|
||||||
|
"extract_invoice_list",
|
||||||
|
"extract_invoice_detail",
|
||||||
|
"format_invoice_data",
|
||||||
|
"run_invoice_rpa",
|
||||||
|
"run_invoice_rpa_with_video",
|
||||||
|
"InvoiceRpaResult",
|
||||||
|
]
|
||||||
104
scripts/service/rpa/browser_session.py
Normal file
104
scripts/service/rpa/browser_session.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
"""浏览器会话管理:基于skill-template的real_browser_rpa示例。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
from typing import Any, Optional, Tuple
|
||||||
|
|
||||||
|
from jiangchang_skill_core import config
|
||||||
|
from jiangchang_skill_core.activity import interruptible_sleep
|
||||||
|
from jiangchang_skill_core.rpa import launch_persistent_browser, poll_until
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# 目标网站配置
|
||||||
|
TARGET_URL = "https://sandbox.jc2009.com/industries/campus/smartcampus/app/finance/invoice-precheck"
|
||||||
|
LOGIN_URL = "https://sandbox.jc2009.com/login"
|
||||||
|
|
||||||
|
|
||||||
|
async def start_browser_session(
|
||||||
|
profile_dir: str,
|
||||||
|
headless: bool = False,
|
||||||
|
) -> Tuple[Any, Any, Any]:
|
||||||
|
"""启动浏览器会话。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
profile_dir: 浏览器配置目录
|
||||||
|
headless: 是否无头模式(生产必须为False)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(playwright, context, page) 三元组
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: 浏览器启动失败
|
||||||
|
"""
|
||||||
|
if not profile_dir:
|
||||||
|
raise RuntimeError("ERROR:MISSING_BROWSER 缺少浏览器配置目录")
|
||||||
|
|
||||||
|
# 确保有头模式(生产要求)
|
||||||
|
if headless:
|
||||||
|
logger.warning("生产环境强制使用有头模式")
|
||||||
|
headless = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 启动持久化浏览器上下文
|
||||||
|
pw, context = await launch_persistent_browser(
|
||||||
|
user_data_dir=profile_dir,
|
||||||
|
headless=headless,
|
||||||
|
args=[
|
||||||
|
"--start-maximized",
|
||||||
|
"--disable-blink-features=AutomationControlled",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建新页面
|
||||||
|
page = await context.new_page()
|
||||||
|
|
||||||
|
# 导航到目标页面
|
||||||
|
await page.goto(TARGET_URL, wait_until="domcontentloaded", timeout=60000)
|
||||||
|
|
||||||
|
logger.info("browser_started url=%s profile_dir=%s", page.url, profile_dir)
|
||||||
|
return pw, context, page
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("browser_start_failed error=%s", str(e))
|
||||||
|
raise RuntimeError(f"ERROR:MISSING_BROWSER 浏览器启动失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
async def close_browser_context(pw: Any, context: Any) -> None:
|
||||||
|
"""安全关闭浏览器上下文。"""
|
||||||
|
try:
|
||||||
|
if context:
|
||||||
|
await context.close()
|
||||||
|
if pw:
|
||||||
|
await pw.stop()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("browser_close_error error=%s", str(e))
|
||||||
|
|
||||||
|
|
||||||
|
async def wait_for_page_ready(page: Any, timeout_sec: float = 30.0) -> bool:
|
||||||
|
"""等待页面加载完成。"""
|
||||||
|
async def _check_ready():
|
||||||
|
try:
|
||||||
|
# 检查页面是否加载完成
|
||||||
|
if await page.evaluate("document.readyState") == "complete":
|
||||||
|
return True
|
||||||
|
# 检查是否有加载指示器
|
||||||
|
loading_selectors = [".loading", ".spinner", "[data-loading]"]
|
||||||
|
for sel in loading_selectors:
|
||||||
|
if await page.locator(sel).count() > 0:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
result = await poll_until(_check_ready, timeout_sec=timeout_sec, interval_sec=0.5)
|
||||||
|
return result.ok
|
||||||
|
|
||||||
|
|
||||||
|
async def random_delay(min_sec: float = 1.0, max_sec: float = 3.0) -> None:
|
||||||
|
"""随机延迟,模拟人类操作节奏。"""
|
||||||
|
delay = random.uniform(min_sec, max_sec)
|
||||||
|
await interruptible_sleep(delay)
|
||||||
349
scripts/service/rpa/invoice_extractor.py
Normal file
349
scripts/service/rpa/invoice_extractor.py
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
"""发票信息提取器:从网站提取发票信息。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from jiangchang_skill_core.activity import interruptible_sleep
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# 发票信息提取选择器(需要根据实际页面调整)
|
||||||
|
INVOICE_SELECTORS = {
|
||||||
|
# 发票列表容器
|
||||||
|
"invoice_list_container": [
|
||||||
|
".invoice-list",
|
||||||
|
".invoice-table",
|
||||||
|
"[data-testid='invoice-list']",
|
||||||
|
"table tbody",
|
||||||
|
],
|
||||||
|
|
||||||
|
# 单个发票行
|
||||||
|
"invoice_row": [
|
||||||
|
"tr.invoice-item",
|
||||||
|
".invoice-row",
|
||||||
|
"[data-testid='invoice-row']",
|
||||||
|
"tbody tr",
|
||||||
|
],
|
||||||
|
|
||||||
|
# 发票详情页面元素
|
||||||
|
"invoice_detail": {
|
||||||
|
"invoice_code": [
|
||||||
|
".invoice-code",
|
||||||
|
"[data-testid='invoice-code']",
|
||||||
|
"td:has-text('发票代码') + td",
|
||||||
|
],
|
||||||
|
"invoice_number": [
|
||||||
|
".invoice-number",
|
||||||
|
"[data-testid='invoice-number']",
|
||||||
|
"td:has-text('发票号码') + td",
|
||||||
|
],
|
||||||
|
"invoice_date": [
|
||||||
|
".invoice-date",
|
||||||
|
"[data-testid='invoice-date']",
|
||||||
|
"td:has-text('开票日期') + td",
|
||||||
|
],
|
||||||
|
"total_amount": [
|
||||||
|
".total-amount",
|
||||||
|
"[data-testid='total-amount']",
|
||||||
|
"td:has-text('金额') + td",
|
||||||
|
],
|
||||||
|
"tax_amount": [
|
||||||
|
".tax-amount",
|
||||||
|
"[data-testid='tax-amount']",
|
||||||
|
"td:has-text('税额') + td",
|
||||||
|
],
|
||||||
|
"buyer_name": [
|
||||||
|
".buyer-name",
|
||||||
|
"[data-testid='buyer-name']",
|
||||||
|
"td:has-text('购买方') + td",
|
||||||
|
],
|
||||||
|
"buyer_tax_no": [
|
||||||
|
".buyer-tax-no",
|
||||||
|
"[data-testid='buyer-tax-no']",
|
||||||
|
"td:has-text('购买方税号') + td",
|
||||||
|
],
|
||||||
|
"seller_name": [
|
||||||
|
".seller-name",
|
||||||
|
"[data-testid='seller-name']",
|
||||||
|
"td:has-text('销售方') + td",
|
||||||
|
],
|
||||||
|
"seller_tax_no": [
|
||||||
|
".seller-tax-no",
|
||||||
|
"[data-testid='seller-tax-no']",
|
||||||
|
"td:has-text('销售方税号') + td",
|
||||||
|
],
|
||||||
|
"items": [
|
||||||
|
".invoice-items",
|
||||||
|
"[data-testid='invoice-items']",
|
||||||
|
".item-list",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
# 分页控件
|
||||||
|
"pagination": {
|
||||||
|
"next_button": [
|
||||||
|
".pagination .next",
|
||||||
|
"button:has-text('下一页')",
|
||||||
|
"[data-testid='next-page']",
|
||||||
|
],
|
||||||
|
"page_info": [
|
||||||
|
".page-info",
|
||||||
|
".pagination-info",
|
||||||
|
"[data-testid='page-info']",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
# 搜索/筛选控件
|
||||||
|
"search": {
|
||||||
|
"search_input": [
|
||||||
|
"input[placeholder*='搜索']",
|
||||||
|
"input[placeholder*='查询']",
|
||||||
|
".search-input",
|
||||||
|
],
|
||||||
|
"search_button": [
|
||||||
|
"button:has-text('搜索')",
|
||||||
|
"button:has-text('查询')",
|
||||||
|
".search-btn",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def extract_invoice_list(page: Any) -> List[Dict[str, Any]]:
|
||||||
|
"""提取发票列表信息。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page: Playwright页面对象
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
发票信息列表
|
||||||
|
"""
|
||||||
|
invoices = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 等待页面加载
|
||||||
|
await interruptible_sleep(2.0)
|
||||||
|
|
||||||
|
# 查找发票列表容器
|
||||||
|
container = await _find_element(page, INVOICE_SELECTORS["invoice_list_container"])
|
||||||
|
if not container:
|
||||||
|
logger.warning("invoice_list_container_not_found")
|
||||||
|
return []
|
||||||
|
|
||||||
|
# 查找所有发票行
|
||||||
|
rows = await _find_elements(page, INVOICE_SELECTORS["invoice_row"])
|
||||||
|
logger.info("found_invoice_rows count=%s", len(rows))
|
||||||
|
|
||||||
|
# 提取每行发票信息
|
||||||
|
for i, row in enumerate(rows):
|
||||||
|
try:
|
||||||
|
invoice_data = await _extract_invoice_from_row(page, row, i)
|
||||||
|
if invoice_data:
|
||||||
|
invoices.append(invoice_data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("extract_row_failed row=%s error=%s", i, str(e))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 处理分页
|
||||||
|
has_next = await _has_next_page(page)
|
||||||
|
while has_next:
|
||||||
|
# 点击下一页
|
||||||
|
next_clicked = await _click_next_page(page)
|
||||||
|
if not next_clicked:
|
||||||
|
break
|
||||||
|
|
||||||
|
await interruptible_sleep(2.0)
|
||||||
|
|
||||||
|
# 提取下一页数据
|
||||||
|
rows = await _find_elements(page, INVOICE_SELECTORS["invoice_row"])
|
||||||
|
for i, row in enumerate(rows):
|
||||||
|
try:
|
||||||
|
invoice_data = await _extract_invoice_from_row(page, row, i)
|
||||||
|
if invoice_data:
|
||||||
|
invoices.append(invoice_data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("extract_row_failed page_row=%s error=%s", i, str(e))
|
||||||
|
continue
|
||||||
|
|
||||||
|
has_next = await _has_next_page(page)
|
||||||
|
|
||||||
|
logger.info("extraction_complete total_invoices=%s", len(invoices))
|
||||||
|
return invoices
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("extract_invoice_list_failed error=%s", str(e))
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
async def extract_invoice_detail(page: Any, invoice_index: int = 0) -> Optional[Dict[str, Any]]:
|
||||||
|
"""提取单个发票的详细信息。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page: Playwright页面对象
|
||||||
|
invoice_index: 发票索引(从0开始)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
发票详细信息字典
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 查找发票行
|
||||||
|
rows = await _find_elements(page, INVOICE_SELECTORS["invoice_row"])
|
||||||
|
if invoice_index >= len(rows):
|
||||||
|
logger.warning("invoice_index_out_of_range index=%s total=%s", invoice_index, len(rows))
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 点击发票行查看详情
|
||||||
|
row = rows[invoice_index]
|
||||||
|
await row.click()
|
||||||
|
await interruptible_sleep(2.0)
|
||||||
|
|
||||||
|
# 提取详细信息
|
||||||
|
detail = {}
|
||||||
|
for field, selectors in INVOICE_SELECTORS["invoice_detail"].items():
|
||||||
|
value = await _extract_text_by_selectors(page, selectors)
|
||||||
|
if value:
|
||||||
|
detail[field] = value
|
||||||
|
|
||||||
|
# 提取商品明细
|
||||||
|
items = await _extract_invoice_items(page)
|
||||||
|
if items:
|
||||||
|
detail["items"] = items
|
||||||
|
|
||||||
|
logger.info("extract_invoice_detail success index=%s fields=%s", invoice_index, list(detail.keys()))
|
||||||
|
return detail
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("extract_invoice_detail_failed error=%s", str(e))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def _extract_invoice_from_row(page: Any, row: Any, index: int) -> Optional[Dict[str, Any]]:
|
||||||
|
"""从行元素提取发票信息。"""
|
||||||
|
try:
|
||||||
|
# 提取行文本
|
||||||
|
row_text = await row.inner_text()
|
||||||
|
|
||||||
|
# 尝试从文本中提取关键信息
|
||||||
|
invoice_data = {
|
||||||
|
"row_index": index,
|
||||||
|
"row_text": row_text.strip(),
|
||||||
|
"raw_text": row_text,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 尝试提取发票号码(通常是数字)
|
||||||
|
invoice_match = re.search(r'(\d{8,20})', row_text)
|
||||||
|
if invoice_match:
|
||||||
|
invoice_data["invoice_number"] = invoice_match.group(1)
|
||||||
|
|
||||||
|
# 尝试提取金额(包含小数点的数字)
|
||||||
|
amount_match = re.search(r'(\d+\.?\d*)', row_text)
|
||||||
|
if amount_match:
|
||||||
|
invoice_data["amount"] = amount_match.group(1)
|
||||||
|
|
||||||
|
return invoice_data
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("extract_from_row_failed index=%s error=%s", index, str(e))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def _extract_invoice_items(page: Any) -> List[Dict[str, str]]:
|
||||||
|
"""提取发票商品明细。"""
|
||||||
|
items = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
item_containers = await _find_elements(page, INVOICE_SELECTORS["invoice_detail"]["items"])
|
||||||
|
if not item_containers:
|
||||||
|
return []
|
||||||
|
|
||||||
|
for container in item_containers:
|
||||||
|
try:
|
||||||
|
item_text = await container.inner_text()
|
||||||
|
items.append({"text": item_text.strip()})
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
async def _has_next_page(page: Any) -> bool:
|
||||||
|
"""检查是否有下一页。"""
|
||||||
|
try:
|
||||||
|
next_button = await _find_element(page, INVOICE_SELECTORS["pagination"]["next_button"])
|
||||||
|
if next_button:
|
||||||
|
# 检查按钮是否可用
|
||||||
|
is_disabled = await next_button.get_attribute("disabled")
|
||||||
|
if is_disabled is None: # 没有disabled属性
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def _click_next_page(page: Any) -> bool:
|
||||||
|
"""点击下一页按钮。"""
|
||||||
|
try:
|
||||||
|
next_button = await _find_element(page, INVOICE_SELECTORS["pagination"]["next_button"])
|
||||||
|
if next_button:
|
||||||
|
await next_button.click()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("click_next_page_failed error=%s", str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def _find_element(page: Any, selectors: list) -> Optional[Any]:
|
||||||
|
"""根据选择器列表查找元素。"""
|
||||||
|
for selector in selectors:
|
||||||
|
try:
|
||||||
|
element = page.locator(selector)
|
||||||
|
if await element.count() > 0 and await element.is_visible():
|
||||||
|
return element
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def _find_elements(page: Any, selectors: list) -> List[Any]:
|
||||||
|
"""根据选择器列表查找多个元素。"""
|
||||||
|
elements = []
|
||||||
|
for selector in selectors:
|
||||||
|
try:
|
||||||
|
found_elements = await page.locator(selector).all()
|
||||||
|
if found_elements:
|
||||||
|
elements.extend(found_elements)
|
||||||
|
break # 找到一组就停止
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return elements
|
||||||
|
|
||||||
|
|
||||||
|
async def _extract_text_by_selectors(page: Any, selectors: list) -> Optional[str]:
|
||||||
|
"""根据选择器列表提取文本。"""
|
||||||
|
for selector in selectors:
|
||||||
|
try:
|
||||||
|
element = page.locator(selector)
|
||||||
|
if await element.count() > 0:
|
||||||
|
text = await element.inner_text()
|
||||||
|
if text and text.strip():
|
||||||
|
return text.strip()
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def format_invoice_data(invoices: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||||
|
"""格式化发票数据为结构化格式。"""
|
||||||
|
return {
|
||||||
|
"total_count": len(invoices),
|
||||||
|
"invoices": invoices,
|
||||||
|
"extraction_complete": True,
|
||||||
|
}
|
||||||
236
scripts/service/rpa/invoice_rpa.py
Normal file
236
scripts/service/rpa/invoice_rpa.py
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
"""发票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
|
||||||
208
scripts/service/rpa/login_handler.py
Normal file
208
scripts/service/rpa/login_handler.py
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
"""登录处理器:处理网站登录流程。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from jiangchang_skill_core.activity import interruptible_sleep
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# 登录相关选择器(需要根据实际页面调整)
|
||||||
|
LOGIN_SELECTORS = {
|
||||||
|
"username_input": [
|
||||||
|
"input[name='username']",
|
||||||
|
"input[placeholder*='用户名']",
|
||||||
|
"input[placeholder*='账号']",
|
||||||
|
"#username",
|
||||||
|
"[data-testid='username']",
|
||||||
|
],
|
||||||
|
"password_input": [
|
||||||
|
"input[name='password']",
|
||||||
|
"input[type='password']",
|
||||||
|
"#password",
|
||||||
|
"[data-testid='password']",
|
||||||
|
],
|
||||||
|
"login_button": [
|
||||||
|
"button[type='submit']",
|
||||||
|
"button:has-text('登录')",
|
||||||
|
"input[type='submit']",
|
||||||
|
".login-btn",
|
||||||
|
"[data-testid='login-button']",
|
||||||
|
],
|
||||||
|
"captcha_input": [
|
||||||
|
"input[name='captcha']",
|
||||||
|
"input[placeholder*='验证码']",
|
||||||
|
"#captcha",
|
||||||
|
],
|
||||||
|
"captcha_image": [
|
||||||
|
".captcha-image",
|
||||||
|
"img[alt*='验证码']",
|
||||||
|
"#captcha-image",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
# 登录成功检测
|
||||||
|
LOGIN_SUCCESS_SELECTORS = [
|
||||||
|
".user-info",
|
||||||
|
".welcome",
|
||||||
|
"[data-testid='user-avatar']",
|
||||||
|
"a[href*='logout']",
|
||||||
|
".dashboard",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def login_if_needed(
|
||||||
|
page: Any,
|
||||||
|
username: str,
|
||||||
|
password: str,
|
||||||
|
wait_sec: int = 120,
|
||||||
|
) -> bool:
|
||||||
|
"""检查登录状态,如需要则执行登录。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page: Playwright页面对象
|
||||||
|
username: 用户名
|
||||||
|
password: 密码
|
||||||
|
wait_sec: 等待人工验证的超时时间
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 登录是否成功
|
||||||
|
"""
|
||||||
|
# 检查是否已登录
|
||||||
|
if await _is_logged_in(page):
|
||||||
|
logger.info("already_logged_in url=%s", page.url)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 导航到登录页面
|
||||||
|
if "/login" not in page.url:
|
||||||
|
await page.goto("https://sandbox.jc2009.com/login", wait_until="domcontentloaded")
|
||||||
|
await interruptible_sleep(2.0)
|
||||||
|
|
||||||
|
# 尝试自动登录
|
||||||
|
login_success = await _try_auto_login(page, username, password)
|
||||||
|
if login_success:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 等待人工登录
|
||||||
|
return await _wait_for_human_login(page, wait_sec)
|
||||||
|
|
||||||
|
|
||||||
|
async def _try_auto_login(page: Any, username: str, password: str) -> bool:
|
||||||
|
"""尝试自动登录。"""
|
||||||
|
try:
|
||||||
|
# 查找用户名输入框
|
||||||
|
username_input = await _find_element(page, LOGIN_SELECTORS["username_input"])
|
||||||
|
if not username_input:
|
||||||
|
logger.warning("username_input_not_found")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 查找密码输入框
|
||||||
|
password_input = await _find_element(page, LOGIN_SELECTORS["password_input"])
|
||||||
|
if not password_input:
|
||||||
|
logger.warning("password_input_not_found")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 输入用户名(模拟人类输入)
|
||||||
|
await username_input.click()
|
||||||
|
await interruptible_sleep(0.5)
|
||||||
|
await username_input.fill("")
|
||||||
|
await page.keyboard.type(username, delay=100)
|
||||||
|
await interruptible_sleep(0.5)
|
||||||
|
|
||||||
|
# 输入密码
|
||||||
|
await password_input.click()
|
||||||
|
await interruptible_sleep(0.5)
|
||||||
|
await password_input.fill("")
|
||||||
|
await page.keyboard.type(password, delay=100)
|
||||||
|
await interruptible_sleep(0.5)
|
||||||
|
|
||||||
|
# 查找并点击登录按钮
|
||||||
|
login_button = await _find_element(page, LOGIN_SELECTORS["login_button"])
|
||||||
|
if not login_button:
|
||||||
|
logger.warning("login_button_not_found")
|
||||||
|
return False
|
||||||
|
|
||||||
|
await login_button.click()
|
||||||
|
await interruptible_sleep(3.0)
|
||||||
|
|
||||||
|
# 检查是否登录成功
|
||||||
|
return await _is_logged_in(page)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("auto_login_failed error=%s", str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def _wait_for_human_login(page: Any, wait_sec: int) -> bool:
|
||||||
|
"""等待人工完成登录。"""
|
||||||
|
print(f"请在浏览器中完成登录,最多等待 {wait_sec} 秒...")
|
||||||
|
deadline = time.monotonic() + wait_sec
|
||||||
|
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
if await _is_logged_in(page):
|
||||||
|
print("检测到登录成功")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 检查是否有验证码需要处理
|
||||||
|
captcha_present = await _check_captcha(page)
|
||||||
|
if captcha_present:
|
||||||
|
print("检测到验证码,请人工完成验证...")
|
||||||
|
|
||||||
|
await interruptible_sleep(2.0)
|
||||||
|
|
||||||
|
print("登录超时")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def _is_logged_in(page: Any) -> bool:
|
||||||
|
"""检查是否已登录。"""
|
||||||
|
try:
|
||||||
|
# 检查URL是否包含登录成功标识
|
||||||
|
if "/login" not in page.url and "/index" in page.url:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 检查页面元素
|
||||||
|
for selector in LOGIN_SUCCESS_SELECTORS:
|
||||||
|
try:
|
||||||
|
element = page.locator(selector)
|
||||||
|
if await element.count() > 0 and await element.is_visible():
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def _check_captcha(page: Any) -> bool:
|
||||||
|
"""检查是否有验证码。"""
|
||||||
|
try:
|
||||||
|
for selector in LOGIN_SELECTORS["captcha_input"]:
|
||||||
|
element = page.locator(selector)
|
||||||
|
if await element.count() > 0 and await element.is_visible():
|
||||||
|
return True
|
||||||
|
|
||||||
|
for selector in LOGIN_SELECTORS["captcha_image"]:
|
||||||
|
element = page.locator(selector)
|
||||||
|
if await element.count() > 0 and await element.is_visible():
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def _find_element(page: Any, selectors: list) -> Optional[Any]:
|
||||||
|
"""根据选择器列表查找元素。"""
|
||||||
|
for selector in selectors:
|
||||||
|
try:
|
||||||
|
element = page.locator(selector)
|
||||||
|
if await element.count() > 0 and await element.is_visible():
|
||||||
|
return element
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return None
|
||||||
248
scripts/service/rpa_invoice_service.py
Normal file
248
scripts/service/rpa_invoice_service.py
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
"""RPA发票数据服务:处理RPA提取的发票数据存储和查询。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from db.connection import get_conn
|
||||||
|
from db.display_metadata import upsert_table_metadata, seed_column_metadata_batch
|
||||||
|
from db.rpa_invoice_schema import RPA_INVOICES_DDL, RPA_INVOICES_INDEX, RPA_INVOICES_COLUMNS
|
||||||
|
from util.constants import LOG_LOGGER_NAME
|
||||||
|
from util.logging_config import get_skill_logger
|
||||||
|
|
||||||
|
|
||||||
|
def init_rpa_invoice_table() -> None:
|
||||||
|
"""初始化RPA发票数据表。"""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 创建表
|
||||||
|
cursor.execute(RPA_INVOICES_DDL)
|
||||||
|
cursor.execute(RPA_INVOICES_INDEX)
|
||||||
|
|
||||||
|
# 添加表元数据
|
||||||
|
upsert_table_metadata(
|
||||||
|
cursor,
|
||||||
|
table_name="rpa_invoices",
|
||||||
|
display_name="RPA发票提取",
|
||||||
|
description="通过RPA从网站提取的发票数据",
|
||||||
|
grid_help="查看RPA提取的发票数据,可按批次ID筛选",
|
||||||
|
visible=1,
|
||||||
|
readonly=1,
|
||||||
|
default_sort_json='[{"column":"id","direction":"desc"}]',
|
||||||
|
default_visible_columns_json='["id","batch_id","extraction_time","username","status","created_at"]',
|
||||||
|
)
|
||||||
|
|
||||||
|
# 添加列元数据
|
||||||
|
seed_column_metadata_batch(
|
||||||
|
cursor,
|
||||||
|
table_name="rpa_invoices",
|
||||||
|
columns=RPA_INVOICES_COLUMNS,
|
||||||
|
valid_column_names=[str(c["column_name"]) for c in RPA_INVOICES_COLUMNS],
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger = get_skill_logger()
|
||||||
|
logger.error("init_rpa_invoice_table_failed error=%s", str(e))
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def save_rpa_invoice_data(
|
||||||
|
batch_id: str,
|
||||||
|
source_url: str,
|
||||||
|
invoice_data: Dict[str, Any],
|
||||||
|
username: str = "",
|
||||||
|
status: str = "extracted",
|
||||||
|
error_message: str = "",
|
||||||
|
) -> int:
|
||||||
|
"""保存RPA提取的发票数据。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
batch_id: 批次ID
|
||||||
|
source_url: 来源网址
|
||||||
|
invoice_data: 发票数据
|
||||||
|
username: 操作用户
|
||||||
|
status: 状态
|
||||||
|
error_message: 错误信息
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
保存的记录ID
|
||||||
|
"""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 将发票数据转换为JSON字符串
|
||||||
|
invoice_data_json = json.dumps(invoice_data, ensure_ascii=False)
|
||||||
|
|
||||||
|
# 插入数据
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO rpa_invoices (
|
||||||
|
batch_id, source_url, username, invoice_data_json, status, error_message
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
""", (batch_id, source_url, username, invoice_data_json, status, error_message))
|
||||||
|
|
||||||
|
record_id = cursor.lastrowid
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
logger = get_skill_logger()
|
||||||
|
logger.info(
|
||||||
|
"rpa_invoice_saved record_id=%s batch_id=%s invoice_count=%s",
|
||||||
|
record_id,
|
||||||
|
batch_id,
|
||||||
|
invoice_data.get("total_count", 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
return record_id
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger = get_skill_logger()
|
||||||
|
logger.error("save_rpa_invoice_data_failed error=%s", str(e))
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def get_rpa_invoice_data(record_id: int) -> Optional[Dict[str, Any]]:
|
||||||
|
"""获取RPA提取的发票数据。"""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT * FROM rpa_invoices WHERE id = ?", (record_id,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 转换为字典
|
||||||
|
columns = [desc[0] for desc in cursor.description]
|
||||||
|
data = dict(zip(columns, row))
|
||||||
|
|
||||||
|
# 解析JSON数据
|
||||||
|
if "invoice_data_json" in data and data["invoice_data_json"]:
|
||||||
|
try:
|
||||||
|
data["invoice_data"] = json.loads(data["invoice_data_json"])
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
data["invoice_data"] = {}
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger = get_skill_logger()
|
||||||
|
logger.error("get_rpa_invoice_data_failed error=%s", str(e))
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def list_rpa_invoices(
|
||||||
|
limit: int = 20,
|
||||||
|
batch_id: Optional[str] = None,
|
||||||
|
username: Optional[str] = None,
|
||||||
|
status: Optional[str] = None,
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""查询RPA提取的发票数据列表。"""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 构建查询条件
|
||||||
|
conditions = []
|
||||||
|
params = []
|
||||||
|
|
||||||
|
if batch_id:
|
||||||
|
conditions.append("batch_id = ?")
|
||||||
|
params.append(batch_id)
|
||||||
|
|
||||||
|
if username:
|
||||||
|
conditions.append("username = ?")
|
||||||
|
params.append(username)
|
||||||
|
|
||||||
|
if status:
|
||||||
|
conditions.append("status = ?")
|
||||||
|
params.append(status)
|
||||||
|
|
||||||
|
where_clause = " AND ".join(conditions) if conditions else "1=1"
|
||||||
|
|
||||||
|
# 执行查询
|
||||||
|
query = f"""
|
||||||
|
SELECT id, batch_id, source_url, extraction_time, username, status, error_message, created_at
|
||||||
|
FROM rpa_invoices
|
||||||
|
WHERE {where_clause}
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT ?
|
||||||
|
"""
|
||||||
|
params.append(limit)
|
||||||
|
|
||||||
|
cursor.execute(query, params)
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
|
# 转换为字典列表
|
||||||
|
columns = [desc[0] for desc in cursor.description]
|
||||||
|
result = [dict(zip(columns, row)) for row in rows]
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger = get_skill_logger()
|
||||||
|
logger.error("list_rpa_invoices_failed error=%s", str(e))
|
||||||
|
return []
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def get_rpa_invoice_stats() -> Dict[str, Any]:
|
||||||
|
"""获取RPA提取统计信息。"""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 总记录数
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM rpa_invoices")
|
||||||
|
total_count = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
# 按状态统计
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT status, COUNT(*) as count
|
||||||
|
FROM rpa_invoices
|
||||||
|
GROUP BY status
|
||||||
|
""")
|
||||||
|
status_stats = {row[0]: row[1] for row in cursor.fetchall()}
|
||||||
|
|
||||||
|
# 最近提取记录
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT id, batch_id, extraction_time, username, status
|
||||||
|
FROM rpa_invoices
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT 5
|
||||||
|
""")
|
||||||
|
recent_extractions = [
|
||||||
|
{
|
||||||
|
"id": row[0],
|
||||||
|
"batch_id": row[1],
|
||||||
|
"extraction_time": row[2],
|
||||||
|
"username": row[3],
|
||||||
|
"status": row[4],
|
||||||
|
}
|
||||||
|
for row in cursor.fetchall()
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total_count": total_count,
|
||||||
|
"status_stats": status_stats,
|
||||||
|
"recent_extractions": recent_extractions,
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger = get_skill_logger()
|
||||||
|
logger.error("get_rpa_invoice_stats_failed error=%s", str(e))
|
||||||
|
return {"error": str(e)}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
@@ -697,3 +697,292 @@ def cmd_stats() -> int:
|
|||||||
log.exception("task_failed task_type=stats")
|
log.exception("task_failed task_type=stats")
|
||||||
print("ERROR:QUERY_FAILED 统计查询异常,详见统一日志")
|
print("ERROR:QUERY_FAILED 统计查询异常,详见统一日志")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# RPA 命令:发票信息提取
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def cmd_extract_invoices(
|
||||||
|
username: str,
|
||||||
|
password: str,
|
||||||
|
profile_dir: str,
|
||||||
|
headless: bool = False,
|
||||||
|
detail_mode: bool = False,
|
||||||
|
) -> int:
|
||||||
|
"""从网站提取发票信息。"""
|
||||||
|
log = _get_task_logger()
|
||||||
|
task_type = "extract_invoices"
|
||||||
|
|
||||||
|
with job_context(skill=SKILL_SLUG):
|
||||||
|
log.info(
|
||||||
|
"task_start task_type=%s username=%s detail_mode=%s",
|
||||||
|
task_type,
|
||||||
|
username[:3] + "***",
|
||||||
|
detail_mode,
|
||||||
|
)
|
||||||
|
emit("开始提取发票信息", skill=SKILL_SLUG, stage="rpa_start")
|
||||||
|
|
||||||
|
try:
|
||||||
|
ok, reason = check_entitlement(SKILL_SLUG)
|
||||||
|
if not ok:
|
||||||
|
log.warning(
|
||||||
|
"entitlement_failed task_type=%s stage=auth error_code=ENTITLEMENT_DENIED",
|
||||||
|
task_type,
|
||||||
|
)
|
||||||
|
emit("鉴权失败,已记录诊断信息", type="warn", skill=SKILL_SLUG, stage="auth")
|
||||||
|
finish(
|
||||||
|
status="failed",
|
||||||
|
message=reason,
|
||||||
|
skill=SKILL_SLUG,
|
||||||
|
error_code="ENTITLEMENT_DENIED",
|
||||||
|
stage="auth",
|
||||||
|
)
|
||||||
|
print(f"ERROR:ENTITLEMENT_DENIED {reason}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
log.info("entitlement_ok task_type=%s stage=auth", task_type)
|
||||||
|
|
||||||
|
# 导入RPA模块
|
||||||
|
from service.rpa import run_invoice_rpa_with_video
|
||||||
|
|
||||||
|
# 运行RPA
|
||||||
|
result = asyncio.run(
|
||||||
|
run_invoice_rpa_with_video(
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
profile_dir=profile_dir,
|
||||||
|
skill_slug=SKILL_SLUG,
|
||||||
|
skill_data_dir=get_skill_root(),
|
||||||
|
headless=headless,
|
||||||
|
detail_mode=detail_mode,
|
||||||
|
title="开始提取发票信息",
|
||||||
|
closing_title="发票信息提取完成",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
invoice_count = result.data.get("total_count", 0)
|
||||||
|
log.info(
|
||||||
|
"rpa_success task_type=%s invoice_count=%s stage=extraction",
|
||||||
|
task_type,
|
||||||
|
invoice_count,
|
||||||
|
)
|
||||||
|
emit(f"成功提取 {invoice_count} 张发票信息", skill=SKILL_SLUG, stage="rpa_complete")
|
||||||
|
|
||||||
|
# 保存到数据库
|
||||||
|
try:
|
||||||
|
from service.rpa_invoice_service import save_rpa_invoice_data
|
||||||
|
|
||||||
|
batch_id = uuid.uuid4().hex[:12]
|
||||||
|
record_id = save_rpa_invoice_data(
|
||||||
|
batch_id=batch_id,
|
||||||
|
source_url="https://sandbox.jc2009.com/industries/campus/smartcampus/app/finance/invoice-precheck",
|
||||||
|
invoice_data=result.data,
|
||||||
|
username=username,
|
||||||
|
status="extracted",
|
||||||
|
)
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"rpa_data_saved record_id=%s batch_id=%s",
|
||||||
|
record_id,
|
||||||
|
batch_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 添加记录ID到结果中
|
||||||
|
result.data["record_id"] = record_id
|
||||||
|
result.data["batch_id"] = batch_id
|
||||||
|
|
||||||
|
except Exception as save_error:
|
||||||
|
log.warning("rpa_data_save_failed error=%s", str(save_error))
|
||||||
|
# 即使保存失败,也继续输出结果
|
||||||
|
|
||||||
|
# 输出结果
|
||||||
|
_print_json(result.data)
|
||||||
|
|
||||||
|
finish(
|
||||||
|
status="success",
|
||||||
|
message=f"成功提取 {invoice_count} 张发票信息",
|
||||||
|
skill=SKILL_SLUG,
|
||||||
|
stage="extraction",
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
log.warning(
|
||||||
|
"rpa_failed task_type=%s error=%s stage=%s",
|
||||||
|
task_type,
|
||||||
|
result.error,
|
||||||
|
result.stage,
|
||||||
|
)
|
||||||
|
emit("发票信息提取失败", type="warn", skill=SKILL_SLUG, stage="rpa_failed")
|
||||||
|
|
||||||
|
finish(
|
||||||
|
status="failed",
|
||||||
|
message=result.error or "发票信息提取失败",
|
||||||
|
skill=SKILL_SLUG,
|
||||||
|
error_code="RPA_FAILED",
|
||||||
|
stage=result.stage or "unknown",
|
||||||
|
)
|
||||||
|
print(f"ERROR:RPA_FAILED {result.error}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
except JobStopped:
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
log.exception(
|
||||||
|
"task_failed task_type=%s",
|
||||||
|
task_type,
|
||||||
|
)
|
||||||
|
emit("任务失败,已记录诊断信息", type="warn", skill=SKILL_SLUG, stage="rpa_error")
|
||||||
|
finish(
|
||||||
|
status="failed",
|
||||||
|
message="发票信息提取异常,详见统一日志",
|
||||||
|
skill=SKILL_SLUG,
|
||||||
|
stage="rpa_error",
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_list_rpa_invoices(
|
||||||
|
limit: int = 20,
|
||||||
|
batch_id: Optional[str] = None,
|
||||||
|
username: Optional[str] = None,
|
||||||
|
status: Optional[str] = None,
|
||||||
|
) -> int:
|
||||||
|
"""查询RPA提取的发票数据。"""
|
||||||
|
log = _get_task_logger()
|
||||||
|
task_type = "list_rpa_invoices"
|
||||||
|
|
||||||
|
with job_context(skill=SKILL_SLUG):
|
||||||
|
log.info(
|
||||||
|
"task_start task_type=%s limit=%s batch_id=%s username=%s status=%s",
|
||||||
|
task_type,
|
||||||
|
limit,
|
||||||
|
batch_id,
|
||||||
|
username,
|
||||||
|
status,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from service.rpa_invoice_service import list_rpa_invoices
|
||||||
|
|
||||||
|
records = list_rpa_invoices(
|
||||||
|
limit=limit,
|
||||||
|
batch_id=batch_id,
|
||||||
|
username=username,
|
||||||
|
status=status,
|
||||||
|
)
|
||||||
|
|
||||||
|
_print_json({
|
||||||
|
"ok": True,
|
||||||
|
"count": len(records),
|
||||||
|
"records": records,
|
||||||
|
})
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("task_failed task_type=%s", task_type)
|
||||||
|
print(f"ERROR:QUERY_FAILED 查询RPA提取数据失败: {str(e)}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_show_rpa_invoice(record_id: int) -> int:
|
||||||
|
"""查看RPA提取的发票数据详情。"""
|
||||||
|
log = _get_task_logger()
|
||||||
|
task_type = "show_rpa_invoice"
|
||||||
|
|
||||||
|
with job_context(skill=SKILL_SLUG):
|
||||||
|
log.info("task_start task_type=%s record_id=%s", task_type, record_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from service.rpa_invoice_service import get_rpa_invoice_data
|
||||||
|
|
||||||
|
record = get_rpa_invoice_data(record_id)
|
||||||
|
if not record:
|
||||||
|
print(f"ERROR:RECORD_NOT_FOUND 未找到记录 ID={record_id}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
_print_json(record)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("task_failed task_type=%s", task_type)
|
||||||
|
print(f"ERROR:QUERY_FAILED 查询RPA发票详情失败: {str(e)}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_rpa_invoice_stats() -> int:
|
||||||
|
"""获取RPA提取统计信息。"""
|
||||||
|
log = _get_task_logger()
|
||||||
|
task_type = "rpa_invoice_stats"
|
||||||
|
|
||||||
|
with job_context(skill=SKILL_SLUG):
|
||||||
|
log.info("task_start task_type=%s", task_type)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from service.rpa_invoice_service import get_rpa_invoice_stats
|
||||||
|
|
||||||
|
stats = get_rpa_invoice_stats()
|
||||||
|
_print_json(stats)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("task_failed task_type=%s", task_type)
|
||||||
|
print(f"ERROR:QUERY_FAILED 获取RPA统计失败: {str(e)}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_test_rpa_connection(
|
||||||
|
username: str,
|
||||||
|
password: str,
|
||||||
|
profile_dir: str,
|
||||||
|
) -> int:
|
||||||
|
"""测试RPA连接和登录。"""
|
||||||
|
log = _get_task_logger()
|
||||||
|
task_type = "test_rpa_connection"
|
||||||
|
|
||||||
|
with job_context(skill=SKILL_SLUG):
|
||||||
|
log.info(
|
||||||
|
"task_start task_type=%s username=%s",
|
||||||
|
task_type,
|
||||||
|
username[:3] + "***",
|
||||||
|
)
|
||||||
|
emit("测试RPA连接", skill=SKILL_SLUG, stage="test_start")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 导入RPA模块
|
||||||
|
from service.rpa import close_browser_context, start_browser_session
|
||||||
|
from service.rpa.login_handler import login_if_needed
|
||||||
|
|
||||||
|
# 启动浏览器
|
||||||
|
pw, context, page = asyncio.run(
|
||||||
|
start_browser_session(profile_dir, headless=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 测试登录
|
||||||
|
login_success = asyncio.run(
|
||||||
|
login_if_needed(page, username, password, wait_sec=30)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 关闭浏览器
|
||||||
|
asyncio.run(close_browser_context(pw, context))
|
||||||
|
|
||||||
|
if login_success:
|
||||||
|
log.info("rpa_connection_test_success task_type=%s", task_type)
|
||||||
|
emit("RPA连接测试成功", skill=SKILL_SLUG, stage="test_success")
|
||||||
|
_print_json({"success": True, "message": "连接和登录测试成功"})
|
||||||
|
finish(status="success", message="RPA连接测试成功", skill=SKILL_SLUG, stage="test")
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
log.warning("rpa_connection_test_failed task_type=%s", task_type)
|
||||||
|
emit("RPA连接测试失败", type="warn", skill=SKILL_SLUG, stage="test_failed")
|
||||||
|
_print_json({"success": False, "message": "登录失败"})
|
||||||
|
finish(status="failed", message="登录失败", skill=SKILL_SLUG, stage="test")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("task_failed task_type=%s", task_type)
|
||||||
|
emit("RPA连接测试异常", type="warn", skill=SKILL_SLUG, stage="test_error")
|
||||||
|
finish(status="failed", message=f"测试异常: {str(e)}", skill=SKILL_SLUG, stage="test")
|
||||||
|
return 1
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""技能标识、版本与平台公共库约束(票据合规预审助手)。"""
|
"""技能标识、版本与平台公共库约束(票据合规预审助手)。"""
|
||||||
|
|
||||||
SKILL_SLUG = "verify-invoice-default"
|
SKILL_SLUG = "verify-invoice-default"
|
||||||
SKILL_VERSION = "1.0.1"
|
SKILL_VERSION = "1.0.2"
|
||||||
LOG_LOGGER_NAME = "openclaw.skill.verify_invoice_default"
|
LOG_LOGGER_NAME = "openclaw.skill.verify_invoice_default"
|
||||||
PLATFORM_KIT_MIN_VERSION = "1.2.11"
|
PLATFORM_KIT_MIN_VERSION = "1.2.11"
|
||||||
|
|||||||
93
test_rpa.py
Normal file
93
test_rpa.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""测试RPA功能。"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 添加scripts目录到Python路径
|
||||||
|
scripts_dir = os.path.join(os.path.dirname(__file__), "scripts")
|
||||||
|
if scripts_dir not in sys.path:
|
||||||
|
sys.path.insert(0, scripts_dir)
|
||||||
|
|
||||||
|
from service.rpa import run_invoice_rpa, close_browser_context
|
||||||
|
from service.rpa.browser_session import start_browser_session
|
||||||
|
|
||||||
|
|
||||||
|
async def test_basic_connection():
|
||||||
|
"""测试基本连接。"""
|
||||||
|
print("测试基本连接...")
|
||||||
|
|
||||||
|
# 注意:需要实际的profile_dir
|
||||||
|
# 这里只是演示结构
|
||||||
|
profile_dir = "test_profile_dir"
|
||||||
|
username = "15527406659"
|
||||||
|
password = "qwer1234"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 启动浏览器
|
||||||
|
pw, context, page = await start_browser_session(profile_dir, headless=False)
|
||||||
|
print("浏览器启动成功")
|
||||||
|
|
||||||
|
# 关闭浏览器
|
||||||
|
await close_browser_context(pw, context)
|
||||||
|
print("浏览器关闭成功")
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"测试失败: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def test_invoice_extraction():
|
||||||
|
"""测试发票提取。"""
|
||||||
|
print("测试发票提取...")
|
||||||
|
|
||||||
|
# 注意:需要实际的profile_dir
|
||||||
|
profile_dir = "test_profile_dir"
|
||||||
|
username = "15527406659"
|
||||||
|
password = "qwer1234"
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await run_invoice_rpa(
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
profile_dir=profile_dir,
|
||||||
|
headless=False,
|
||||||
|
detail_mode=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
print(f"提取成功: {result.data}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"提取失败: {result.error}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"测试异常: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""主测试函数。"""
|
||||||
|
print("=" * 50)
|
||||||
|
print("RPA功能测试")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# 运行测试
|
||||||
|
success = asyncio.run(test_basic_connection())
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print("\n基本连接测试通过")
|
||||||
|
else:
|
||||||
|
print("\n基本连接测试失败")
|
||||||
|
|
||||||
|
print("\n" + "=" * 50)
|
||||||
|
print("注意:实际运行需要有效的浏览器配置目录")
|
||||||
|
print("请确保已通过account-manager获取profile_dir")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user