Install
openclaw skills install @sangjie123/travel-expense-reimbursement自动化差旅费用报销流程,支持12306火车票、携程机票/酒店、华住会酒店、美团、吉祥航空、百望等电子发票的自动获取、解析和报销提交。支持邮件附件下载和链接下载两种发票获取方式。触发条件:用户提及差旅报销、火车票报销、12306发票、携程发票、华住会发票、酒店报销、机票报销、电子客票、美团发票、航空发票、百望发票等。
openclaw skills install @sangjie123/travel-expense-reimbursement📊 完整流程图见
references/flowchart.md,建议先阅读流程图了解整体流程。
| 技能/模块 | 用途 |
|---|---|
| url_downloader.py | 给定 URL,curl 优先下载 → Playwright 兜底(邮件正文中发票链接的下载) |
| pdf-ocr | PDF/图片OCR文字提取、发票信息解析 |
| browser | 浏览器自动化(仅用于 FOL 报销系统,发票上传和报销单提交) |
| fol-invoice-upload | FOL发票上传,通过浏览器自动化将发票PDF上传到财务系统并完成OCR识别 |
| fol-travel-expense | FOL报销申请,通过浏览器自动化创建国内差旅报销单并提交 |
| himalaya | 邮件搜索、下载附件(直接下载邮件中的 PDF/OFD 附件) |
所有涉及 IMAP 连接的操作必须严格串行执行,禁止并发调用同一邮箱账号。
imaplib.IMAP4_SSL() 操作完成后记得 mail.logout()OCR 解析必须一张一张串行执行,禁止并发/并行调用多个 OCR 进程。
RapidOCR 每进程需加载模型占用 350MB+ 内存,并行会导致内存暴涨触发 OOM Killer 杀死 gateway 进程。
工作目录:/tmp/openclaw/uploads/{session_id}/(每次报销使用独立的动态子目录,如 /tmp/openclaw/uploads/run-20260529-001/)。
创建子目录:
/tmp/openclaw/uploads/{session_id}/
├── inbox/ # 所有发票PDF统一存放,不做分类子目录
├── processed/ # 已解析的发票数据
└── output/ # 生成的报销文件
动态目录命名规则:
run-{YYYYMMDD}-{序号},先检查/tmp/openclaw/uploads/下已有目录避免冲突。
四步判断流程(严格按顺序执行):
通过 himalaya envelope list 获取今日邮件列表,根据发件人和主题过滤出发票邮件。
对每封邮件判断:himalaya message read <msg_id> 查看邮件结构,确认是否有 PDF/OFD 附件(has_attachment: true 的邮件)。
对有附件的邮件,执行:
himalaya attachment download <message_id> -d /tmp/openclaw/uploads/{session_id}/inbox/
附件直接落入 inbox/,标记该邮件处理完毕,跳过第三、四步。
对无附件的邮件,读取正文(himalaya message read <msg_id>),按以下优先级提取下载 URL:
re.findall(r'https?://[^\s"'\'<>]+', text) 从 HTML 正文直接匹配 http 链接email 模块解析原始邮件,对每个 part 的 text/html 或 text/plain 内容深度扫描 URL常见发件人 URL 特征:
pis.baiwang.com/smkp-vue/previewInvoiceAllEle?param=einvoice@juneyaoair.com / www.juneyaoair.com/eflight/invoice调用 url_downloader.py 下载:
import sys
sys.path.insert(0, '/app/skills/travel-expense-reimbursement/references')
from url_downloader import download_file
filepath = download_file(
url='<提取到的URL>',
save_dir='/tmp/openclaw/uploads/{session_id}/inbox/',
filename_hint='invoice.pdf',
)
下载策略(curl 优先,Playwright 兜底):
curl 直接下载
↓ 失败(超时/非PDF响应)
Playwright 访问页面
→ 页面直接触发 PDF 下载?拦截保存
→ 未触发?查找并点击下载按钮
→ 找到 PDF 直链?curl 下载
→ 全部失败 → 截图备用,告知用户手动下载
扩展新的发票下载类型(在 url_downloader.py 中添加 handler):
def download_myinvoice(page_url: str, save_dir: str) -> Optional[str]:
return download_file(page_url, save_dir)
URL_PATTERN_HANDLERS.append((
'https://myinvoice.example.com/',
download_myinvoice,
))
⚠️ 吉祥航空特别说明:部分吉祥航空邮件正文中的"下载PDF文件"仅为文字按钮,非真实超链接。若第二步和第三步均无法获取真实下载地址,应告知用户手动从吉祥航空App或官网下载发票附件后发送至邮箱。
批量解析:所有 PDF 一次性完成,不逐张调用。
import sys
import os
import glob
sys.path.insert(0, '/app/skills/pdf-ocr-skill/scripts')
from pdf_ocr_processor import PDFOCRProcessor
inbox_dir = '/tmp/openclaw/uploads/{session_id}/inbox/'
pdf_files = sorted(glob.glob(os.path.join(inbox_dir, '*.pdf')))
processor = PDFOCRProcessor(engine='rapid')
results = []
for pdf_path in pdf_files:
result = processor.ocr_pdf(pdf_path)
results.append({
'pdf_path': pdf_path,
'filename': os.path.basename(pdf_path),
'text': result['text'],
'page_count': result['page_count']
})
print(f"[OK] {os.path.basename(pdf_path)} ({result['page_count']}页)")
# 输出汇总供后续步骤使用
import json
print('\n=== OCR_RESULTS_JSON ===')
print(json.dumps(results, ensure_ascii=False))
results 列表中,进入步骤5汇总/tmp/openclaw/uploads/{session_id}/processed/invoice-list.json依据 references/行程分析指南.md 分析行程,先自行分析整理,遇到不确定的再问用户。
以下参数从发票自动推断,不需要问用户:
只需要确认两个参数:报销事由和项目名称。
通过 memory_search 搜索历史报销记忆,关键词:报销 项目名称、差旅报销 事由
向用户一次性确认:
请确认:
- 报销事由:出差(记忆中最常用)/ 或用户输入
- 项目名称:????(历史曾用:A、B、C)/ 或用户输入新项目
其余参数不询问,从发票自动填充。
用户确认后,只将事由和项目名称写入 memory/YYYY-MM-DD.md:
## 差旅报销 (当日日期)
- 事由: XXX
- 项目名称: XXX
调用 fol-invoice-upload 技能,一次性批量上传所有发票PDF(不要逐张上传)。
收集所有待上传发票的文件路径列表,一次性传递给上传流程(如果上传流程支持批量),或尽可能连续快速上传减少等待。
发票文件路径使用 /tmp/openclaw/uploads/{session_id}/inbox/ 下的文件。
调用 fol-travel-expense 技能,按行程分段提交报销单。
向用户展示:处理发票数、总金额、行程汇总、提交状态。
| 错误 | 处理 |
|---|---|
| 第一步判断失误/邮件获取失败 | 降级到 Python imaplib 直连获取 |
| 第二步 himalaya 下载附件失败 | 降级到 Python imaplib 直连 |
| 第三步 无法提取下载地址(按钮文字非链接) | 告知用户手动从发件方官网/App下载,询问是否上传PDF |
| 第三步 正则提取 URL 失败 | 尝试 Python email 模块深度解析 MIME 结构 |
| 第四步 url_downloader curl 失败 | 自动切换 Playwright 浏览器方案 |
| 第四步 Playwright 仍失败(超时/需登录/验证码) | 截图备用,告知用户该链接无法自动下载,询问是否手动上传PDF |
| PDF解析失败 | 保留文件,继续处理其他的 |
| 发票类型无法识别 | 记录到待处理,提示用户 |
| fol-invoice-upload 失败 | 记录失败发票,提示手动上传 |
| fol-travel-expense 失败 | 保存数据,提示手动提交 |
| 发票类型 | 搜索关键词(from) | 主题特征 | 发票获取方式 |
|---|---|---|---|
| 12306火车票 | 12306 | "电子发票通知" | 邮件附件 |
| 携程机票/酒店 | trip | "机票"/"行程单"/"航班"/"酒店"/"住宿" | 邮件附件 |
| 去哪网机票/酒店 | qunar | "机票"/"行程单"/"航班"/"酒店"/"住宿" | 邮件附件 |
| 华住会酒店 | huazhuhotel | "发票"/"电子发票" | 邮件附件 |
| 美团发票 | meituan | "发票"/"电子发票" | 邮件附件 |
| 吉祥航空机票 | juneyaoair | 航空/行程单/发票 | url_downloader.py(curl 直链,失败切 Playwright) |
| 百望发票 | baiwang | 发票/电子发票 | url_downloader.py(GreenPaper 跨域 iframe,截图备用;建议手动下载 PDF 上传) |