Install
openclaw skills install @leahlu0124-creator/invoice-from-email从邮箱自动搜索下载发票/行程单附件,智能三级文字提取(PyMuPDF/Tesseract/PaddleOCR自动降级),发票+行程单合并排版到A4纸,生成双Sheet费用清单Excel。零OCR依赖即可处理90%+电子发票。
openclaw skills install @leahlu0124-creator/invoice-from-email从指定邮箱搜索发票邮件 → 下载附件 → 本地智能提取(三级自动选最优)→ 合并发票+行程单PDF → 生成费用清单 Excel → 放到桌面。
scripts/ 目录,无需额外安装本技能的核心设计原则:能用文字层直读就不用 OCR,能用轻量 OCR 就不用重型 OCR。
pip install PyMuPDF openpyxl
PyMuPDF 是 merge_invoices.py 的硬依赖,也是 PDF 文字提取的主力(L1)。openpyxl 用于生成 Excel。
# macOS
brew install tesseract
# 只下载中文语言包(2.4MB),不要装 tesseract-lang(会装全部 163 种语言 654MB)
curl -L -o /opt/homebrew/share/tessdata/chi_sim.traineddata \
https://github.com/tesseract-ocr/tessdata/raw/main/chi_sim.traineddata
pip install pytesseract
# Windows(一条路)
# 1. 下载安装 Tesseract: https://github.com/UB-Mannheim/tesseract/wiki
# 安装时勾选 "Chinese Simplified" 语言包
# 2. pip install pytesseract
# Linux
sudo apt install tesseract-ocr tesseract-ocr-chi-sim
pip install pytesseract
Tesseract + 中文语言包约 50MB,能处理扫描件/图片型 PDF。
| 增强项 | 用途 | 安装量 | 何时装 |
|---|---|---|---|
| PaddleOCR | 最佳中文 OCR 精度 | ~600MB | 大批量处理、复杂扫描件 |
| MinerU | 云端 API,处理表格/公式 | npm 全局包 | 需要云端处理能力时 |
💡 90%+ 的电子发票走 PyMuPDF 文字层直读(L1),以上两项均不需要。
ocr_extract.py 对每个 PDF 自动选择最佳方法,无需用户干预:
| 级别 | 方法 | 何时触发 | 速度 | 跨平台 |
|---|---|---|---|---|
| L1 | PyMuPDF 直接读文字层 | 默认(电子发票文字型 PDF) | 毫秒级 | ✅ |
| L2 | Tesseract OCR | 文字层为空(扫描件),且 Tesseract 已装 | 秒级 | ✅ |
| L3 | PaddleOCR | L1/L2 均失败,且 PaddleOCR 已装 | 十秒级 | ✅ |
提取脚本:scripts/ocr_extract.py
python ocr_extract.py <发票.pdf> <输出.md>
# 自动输出用了哪一级:PyMuPDF 文字层 / Tesseract OCR / PaddleOCR
在开始工作流程之前,依次执行以下检查:
cat /Users/lulingyan/.workbuddy/skills/imap-smtp-email/.env 2>/dev/null
.env 存在且 IMAP_PASS= 非空 → 通过。否则暂停,执行下方「首次使用:邮箱授权引导」。
# PyMuPDF(merge_invoices.py + 文字提取 L1 共用)
python3 -c "import fitz; print('✅ PyMuPDF', fitz.version[0])" 2>/dev/null || echo "❌ 缺少 PyMuPDF:pip install PyMuPDF"
# openpyxl(Excel 生成)
python3 -c "import openpyxl; print('✅ openpyxl')" 2>/dev/null || echo "❌ 缺少 openpyxl:pip install openpyxl"
# Tesseract(L2 扫描件 OCR)
which tesseract 2>/dev/null && tesseract --list-langs 2>/dev/null | grep -q chi_sim && echo "✅ Tesseract + 中文" || echo "ℹ️ Tesseract 未装(扫描件走 L3 或跳过)"
# PaddleOCR(L3 深度 OCR,有专用 venv)
PADDLE_VENV=/Users/lulingyan/.workbuddy/binaries/python/envs/paddleocr
$PADDLE_VENV/bin/python -c "from paddleocr import PaddleOCR; print('✅ PaddleOCR')" 2>/dev/null || echo "ℹ️ PaddleOCR 未装"
# MinerU(可选云端 API)
which mineru-open-api 2>/dev/null && echo "✅ MinerU" || echo "ℹ️ MinerU 未装"
ls /Users/lulingyan/.workbuddy/skills/invoice-from-email/scripts/merge_invoices.py 2>/dev/null && echo "✅ merge_invoices.py" || echo "❌ 缺少 merge_invoices.py"
按以下步骤引导新用户完成配置(用自然语言与用户交互,不要直接写文件):
第1步:询问邮箱地址
"请问你的发票邮箱地址是?(例如 yourname@qq.com)"
第2步:根据邮箱后缀判断服务商
| 后缀 | IMAP Host | 端口 | SMTP Host | 端口 | 密码类型 |
|---|---|---|---|---|---|
| @163.com / @vip.163.com | imap.163.com | 993 | smtp.163.com | 465 | 授权码 |
| @126.com / @vip.126.com | imap.126.com | 993 | smtp.126.com | 465 | 授权码 |
| @qq.com | imap.qq.com | 993 | smtp.qq.com | 587 | 授权码 |
| @gmail.com | imap.gmail.com | 993 | smtp.gmail.com | 587 | App Password |
| @outlook.com | outlook.office365.com | 993 | smtp.office365.com | 587 | 正常密码 |
第3步:提醒获取授权码(关键!)
根据服务商提示用户:
imap-smtp-email SKILL.md 的 Configuration 章节第4步:帮用户生成 .env 文件
收集到邮箱地址和授权码后,用 Write 工具写入:
路径:/Users/lulingyan/.workbuddy/skills/imap-smtp-email/.env
(参考 imap-smtp-email SKILL.md 中 Configuration 章节的模板)
第5步:测试连接
cd /Users/lulingyan/.workbuddy/skills/imap-smtp-email
node scripts/imap.js check --limit 1
常见错误:
| 报错 | 原因 | 解决方案 |
|---|---|---|
Authentication failed | 授权码错误,或未开启 IMAP | 重新生成授权码,确认 IMAP 已开启 |
Connection timeout | 主机名/端口错误 | 核对上表,确认网络可访问对应端口 |
在正式开始搜索邮件之前,必须先用一句话提醒用户:
⚠️ 请确认发到邮箱里的发票/行程单附件是 PDF 格式。OFD、XML、图片等格式无法自动处理。如同时收到 PDF + OFD,PDF 已满足报销需求,OFD 直接跳过。
原则:不用 --limit 截断,先搜发票关键词再搜行程单关键词,合并去重,确保不漏。
cd /Users/lulingyan/.workbuddy/skills/imap-smtp-email
# 按发票相关主题搜(不加 limit,全部返回)
node scripts/imap.js search --since 2026-06-01 --before 2026-07-01 --subject "发票" --limit 500
# 按行程单/报销相关主题搜
node scripts/imap.js search --since 2026-06-01 --before 2026-07-01 --subject "报销" --limit 500
node scripts/imap.js search --since 2026-06-01 --before 2026-07-01 --subject "行程" --limit 500
# 如用户指定"最近一周"等短时间范围,可用 --recent 代替日期
node scripts/imap.js search --recent 7d --subject "发票" --limit 500
⚠️ 不要用
--limit 50:一个月可能有数百封邮件,低 limit 会截断结果导致漏票。用--limit 500或直接省略(IMAP 按时间范围返回全部)。
从输出中筛出含发票/行程单 PDF 附件的邮件,记录其 UID。重点关注以下发件人:
去重:检查工作目录的 .processed_uids 文件(如存在),跳过已处理的 UID:
cat <工作目录>/.processed_uids 2>/dev/null
工作目录:<当前 workspace>/invoices_YYYYMMDD/(按当天日期命名,例如 /Users/lulingyan/WorkBuddy/2026-07-14-16-38-40/invoices_20260714/)
mkdir -p <工作目录>
# 逐封邮件下载全部附件
node scripts/imap.js download <UID> --dir <工作目录>
下载完成后,记录已处理的 UID:
echo "<UID>" >> <工作目录>/.processed_uids
cd <工作目录>
unzip -o *.zip 2>/dev/null
# 如果 zip 文件名含中文乱码(常见于 12306 邮件),用 Python 解压:
python3 -c "
import zipfile, os
for zf_name in [f for f in os.listdir('.') if f.endswith('.zip')]:
z = zipfile.ZipFile(zf_name)
for info in z.infolist():
name = info.filename.encode('cp437').decode('gbk', errors='replace')
if name.lower().endswith('.pdf'):
with z.open(info) as src, open(name, 'wb') as dst:
dst.write(src.read())
print(f'Extract: {name}')
"
解压后的 PDF 通常为纯数字文件名(12306 火车票),或含行程内容的 PDF。
对工作目录下所有 PDF,提取 → 解析 → 输出 JSON,全程脚本完成,AI 不参与字段解析。
TS=$(date +%s)
EXTRACT=/Users/lulingyan/.workbuddy/skills/invoice-from-email/scripts/ocr_extract.py
PARSE=/Users/lulingyan/.workbuddy/skills/invoice-from-email/scripts/parse_invoice.py
i=1
for pdf in <工作目录>/*.pdf; do
echo "=== $(basename "$pdf") ==="
# 提取文本
python3 "$EXTRACT" "$pdf" "/tmp/invoice_${TS}_${i}.md"
# 结构化解析
python3 "$PARSE" "/tmp/invoice_${TS}_${i}.md" -o "/tmp/invoice_${TS}_${i}.json"
i=$((i+1))
done
输出示例:
{
"type": "invoice",
"file": "【高德打车-20260714】电子发票.pdf",
"invoice_number": "2026071412345678",
"invoice_date": "2026-07-14",
"seller_name": "北京高德云图科技有限公司",
"amount_excluding_tax": 25.47,
"tax_amount": 1.53,
"total_amount": 27.00
}
{
"type": "train",
"file": "G123_001.pdf",
"train_number": "G123",
"departure_station": "无锡站",
"arrival_station": "上海虹桥站",
"seat_type": "二等座",
"amount": 59.50
}
AI 只需读取所有 /tmp/invoice_*.json 文件,直接填入 Excel,无需手工逐字段解析。
💡 如果 MinerU 已装且想用它处理特定文件,可以先跑
mineru-open-api flash-extract出 .md,再跑parse_invoice.py解析。
/usr/bin/python3 /Users/lulingyan/.workbuddy/skills/invoice-from-email/scripts/merge_invoices.py <工作目录> <工作目录>/merged
脚本会自动:
merged/ 目录(发票在上、行程单在下)注意:
merge_invoices.py不再支持--excel参数。Excel 由第六步独立生成,数据源为第四步提取结果。
12306 纯数字文件名火车票不会自动配对,需在第六步 Excel 中单独列出行程信息,合并 PDF 时使用下方手动方式。
手动合并两张火车票(12306): 用 PyMuPDF 将两张 PDF 上下拼合到一张 A4:
import fitz
a = fitz.open("票1.pdf")
b = fitz.open("票2.pdf")
out = fitz.open()
page = out.new_page(width=595, height=842)
page.show_pdf_page(fitz.Rect(0, 0, 595, 421), a, 0)
page.show_pdf_page(fitz.Rect(0, 421, 595, 842), b, 0)
out.save("merged/12306火车票_往返_合并.pdf")
generate_excel.py 读取第四步产出的所有 JSON 文件,自动分组配对、自动检测金额差异,生成双 Sheet Excel。
# JSON 目录为第四步写入的 /tmp/(与 extract 同目录)
/usr/bin/python3 /Users/lulingyan/.workbuddy/skills/invoice-from-email/scripts/generate_excel.py /tmp/ <工作目录>/费用清单.xlsx
AI 无需手写 openpyxl。 脚本自动完成:
# 如果同名文件夹已存在,加时间戳后缀
DESKTOP_DIR=~/Desktop/发票整理_$(date +%Y%m%d)
if [ -d "$DESKTOP_DIR" ]; then
DESKTOP_DIR="${DESKTOP_DIR}_$(date +%H%M)"
fi
mkdir -p "$DESKTOP_DIR"
# 将 merged/ 所有文件和 Excel 移入桌面文件夹
cp <工作目录>/merged/* "$DESKTOP_DIR/"
cp <工作目录>/*.xlsx "$DESKTOP_DIR/" 2>/dev/null
命名规则:文件夹名 =
发票整理_YYYYMMDD,如果当天已存在则追加时间发票整理_YYYYMMDD_HHMM
# 清理临时提取和解析文件
rm -f /tmp/invoice_*.md /tmp/invoice_*.json
# 清理非 PDF/Excel 中间文件
cd <工作目录>
/usr/bin/trash *.ofd *.zip *.xml 2>/dev/null
如果用户说"可以删了"或"清理一下",则删除整个工作目录(用 trash):
/usr/bin/trash <工作目录>
merge_invoices.py 不会自动配对纯数字文件,需手动合并两张往返票。火车票数据需流入 Sheet2「行程单汇总」。~/.workbuddy/skills/imap-smtp-email/.env 获取邮箱地址和密码<工作目录>/.processed_uids,下次运行自动跳过.processed_uids 文件保留在工作目录中,重新执行时会跳过已处理邮件