← 返回 Skills 市场
lorwaleroy

Expense Reimbursement

作者 LorwaLeroy · GitHub ↗ · v3.0.4 · MIT-0
cross-platform ⚠ suspicious
125
总下载
1
收藏
0
当前安装
16
版本数
在 OpenClaw 中安装
/install expense-reimbursement
功能描述
差旅报销票据整理 / Travel Expense Reimbursement:支持高德/Didi/滴滴/Uber打车、12306火车票、机票、酒店住宿;递归扫描含ZIP内部;OCR识别行程单;判断出差/公出类型;按行程自动归档;填写研发经费使用单;生成打印材料包PDF(粘贴单永远首页);自动化PDF合并(图片+...
使用说明 (SKILL.md)

差旅报销整理技能 v3.0

Travel Expense Reimbursement Assistant v3.0


这个技能能做到什么?

把报销从"手动分类归档"变成"AI自动完成",再到最后一步"打印材料包生成",全程每个关键节点需要你确认后才继续。

核心能力

  • Step 0-6:票据扫描 → 分类归档 → 填写表单 → 汇报确认(必须等你说"确认"才完成)
  • Step 7:粘贴单核查 → 生成打印材料包 PDF(粘贴单首页)→ 汇报页数 → 等你说"确认"
  • Step 8:签字完成后收尾,更新最终版打印材料包

自动化合并(关键能力)

Python 代码支持:

  • 表单 docx → PDF(python-docx + reportlab Table 重建,完全本地)
  • 所有票据 PDF → 直接合并(打印机自动适配 A4,不做强制缩放)
  • 图片行程单截图 → 转为 PDF(等比缩放 A4 页面)
  • 全程自动跳过申请截图、.DS_Store 等无关文件

能力前提

依赖 用途
python-docx 读 Word 表单内容
pypdf 合并多个 PDF
reportlab 图片 → A4 PDF(等比缩放)
Pillow 图片尺寸读取
tesseract(Linux/Windows) OCR;macOS 用内置 qlmanage
zipfile(标准库) 解压 ZIP
pip install python-docx pypdf reportlab Pillow

核心概念

出差 vs 公出

类型 判断 补贴
出差 外出 > 1天(在外过夜) 有差旅补贴
公出 外出 ≤ 1天(当日往返) 无补贴

以申请截图时间为准,不自作主张。

票据材料分类

类型 处理方式
粘贴单(BEWG/截圖) PDF,第1页固定
研发经费使用单 python-docx + reportlab Table 渲染 PDF,第2页
打车发票 PDF 第3页起,发票+行程单配对
火车票 PDF 自带行程信息,无需配对,放在最后
申请截图 PNG 跳过,不加入打印包

标准工作流程

用户: "帮我整理报销"
  ↓
【阶段一:归档】
Step 0-6 → 汇报 → 等你说"确认" → 完成
  ↓
【阶段二:打印材料】
用户: "生成打印材料"
  ↓
Step 7.1 粘贴单核查 → 汇报完整性
  ⚠️ 等你说"确认"
  ↓
Step 7.3 生成打印材料包 → 汇报页数
  ⚠️ 等你说"确认"
  ↓
Step 7.4 创建扫描打印文件夹 → 汇报完成
  ↓
用户: "签字完成"
  ↓
【阶段三:收尾】
Step 8 → 更新签字版 → 汇报完成

Step 0 — 读取申请截图

OCR 提取:出差/公出类型、出发日期、返回日期、目的地、事由。


Step 1 — 递归扫描所有票据

含 ZIP 内部,完整扫描,不遗漏。


Step 2 — 识别票据类型,解析内容

打车发票:XML 解析 \x3CSellerAddr> 判断真实城市。 火车票:OCR 提取出发地/到达地/车次/时间/金额。 行程单图片qlmanage -t -s 1200 + image 工具 OCR。


Step 3 — 判断出差/公出

出发日期和返回日期相差 > 1天 → 出差;≤ 1天 → 公出。


Step 4 — 按行程分类归档

报销/
├── 00_原始资料/               ← 备份,不参与主流程
├── 01_3月11日无锡出差/
│   ├── 申请截图/
│   ├── 交通票据/
│   │   ├── 01_出发打车/
│   │   ├── 02_火车票/
│   │   └── 03_返程打车/
│   ├── 研发经费使用单_北水科技_XXX.docx
│   ├── [粘贴单]/
│   └── 扫描打印文件/          ← Step 7.4 新建
├── ...
└── 领导签字用/               ← 表单签字前集中放这里

Step 5 — 填写研发经费使用单

必须先问用户使用哪个专项项目(课题编号),确认后填写。

from docx import Document

doc = Document(template_path)
table = doc.tables[0]

def set_cell(cell, text):
    for p in cell.paragraphs:
        for r in p.runs:
            r.text = ""
    if cell.paragraphs[0].runs:
        cell.paragraphs[0].runs[0].text = str(text)
    else:
        cell.paragraphs[0].add_run(str(text))

# 费用明细(只写有费用的项目,金额=0不写入)
items = []
if taxi_total > 0:
    items.append(f"其中市内交通费共计 {taxi_total}元")
if train_total > 0:
    items.append(f"高铁费用 {train_total}元")
if hotel_total > 0:
    items.append(f"住宿费用 {hotel_total}元")
if allowance > 0:
    items.append(f"差旅补贴 {allowance}元")
detail = "因课题研发需要,产生交通费用\
" + "\
".join(items) + f"\
共计{total}元"
for col in [1, 2, 3, 4]:
    set_cell(table.rows[2].cells[col], detail)

for col in [1, 2, 3, 4]:
    set_cell(table.rows[3].cells[col], f"{total}元(大写:...)")

doc.save(output_path)

Step 5.5 — 归档完成汇报(必须等确认)

汇报所有行程的:类型、金额、票据数量、归档路径。

✅ 归档完成,请确认:
  01_3月2日杭州公出:    ¥130.24  |  4 PDF
  02_3月11日无锡出差:  ¥556.61  |  8 PDF + 2火车票
  04_3月23日哈尔滨公出: ¥285.62  |  8 PDF
  05_3月30日北京公出:   ¥168.92  |  6 PDF
  06_4月2日上海公出:     ¥236.70  |  6 PDF + 2火车票
  合计: ¥1,378.09

⚠️ 回复「确认」后完成归档。

Step 5.6 — 原始文件清理(需用户授权)

"原始文件已备份在 00_原始资料/。回复『删除』清理 ZIP 和临时文件,回复『保留』保留全部。"


Step 6 — 创建对话摘要

路径:~/memory/YYYY-MM-DD_报销整理.md


Step 7 — 打印材料生成(分段确认)

Step 7.1 — 检查粘贴单据完整性

自动执行,立即汇报:

import os, glob

def check_paste_lists(reimburs_dir):
    """批量检查所有行程的粘贴单"""
    results = {}
    trip_dirs = sorted([d for d in os.listdir(reimburs_dir)
                        if d.startswith(("0","1","2")) and ("出差" in d or "公出" in d)])
    for trip in trip_dirs:
        trip_dir = os.path.join(reimburs_dir, trip)
        found = None
        for pat in ["BEWG*.pdf", "截圖*.pdf", "截圖 *.pdf"]:
            files = glob.glob(os.path.join(trip_dir, "*粘贴单*", pat), recursive=True)
            files += glob.glob(os.path.join(trip_dir, "**", pat), recursive=True)
            files = [f for f in files if f.endswith('.pdf') and os.path.getsize(f) > 50000]
            if files:
                found = max(files, key=lambda f: os.path.getsize(f))
                break
        results[trip] = found
    return results

汇报格式:

╔══════════════════════════════════════════════════════╗
║              粘贴单据完整性核查                    ║
╚══════════════════════════════════════════════════════╝

✅ 01_3月2日杭州公出:      BEWG-差旅报销单-粘贴单.pdf(55KB)
✅ 02_3月11日无锡出差:    BEWG-差旅报销单-粘贴单.pdf(55KB)
✅ 04_3月23日哈尔滨公出:  截圖 2026-03-28 下午5.53.04.pdf(55KB)
✅ 05_3月30日北京公出:    BEWG-差旅报销单-粘贴单.pdf(55KB)
✅ 06_4月2日上海公出:      BEWG-差旅报销单-粘贴单.pdf(55KB)

结论:5/5 完整。

⚠️ 停顿,等待用户回复「确认」后继续生成打印材料包。


Step 7.2 — 检查表单签字状态

汇报签字状态:

╔══════════════════════════════════════════════════════╗
║              表单签字状态核查                       ║
╚══════════════════════════════════════════════════════╝

⚠️ 01_3月2日杭州公出:      表单在「领导签字用」文件夹,尚未签字
⚠️ 02_3月11日无锡出差:    表单在「领导签字用」文件夹,尚未签字
⚠️ 04_3月23日哈尔滨公出:  表单在「领导签字用」文件夹,尚未签字
⚠️ 05_3月30日北京公出:    表单在「领导签字用」文件夹,尚未签字
⚠️ 06_4月2日上海公出:      表单在「领导签字用」文件夹,尚未签字

→ 签字后再执行 Step 8 收尾。

Step 7.3 — 生成打印材料包 PDF

用户回复「确认」后执行,自动合并所有材料为一个 PDF:

Python 实现(核心合并逻辑):

import os, glob
from pypdf import PdfMerger, PdfReader
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from PIL import Image as PILImage

W, H = A4                       # 210 × 297mm
MARGIN = 12 * mm               # 边距 12mm

# ── 图片 → A4 PDF(等比缩放,不超边距,居中)─────────────
def img_to_pdf(img_path, out_path):
    img = PILImage.open(img_path)
    iw, ih = img.size
    avail_w = W - 2 * MARGIN
    avail_h = H - 2 * MARGIN
    ratio = min(avail_w / iw, avail_h / ih)
    nw, nh = iw * ratio, ih * ratio
    x = (W - nw) / 2
    y = (H - nh) / 2
    c = canvas.Canvas(out_path, pagesize=A4)
    c.drawImage(img_path, x, y, nw, nh)
    c.save()

# ── Word → PDF(python-docx + reportlab Table 渲染)────────────
# 使用 python-docx 读取表单内容 + reportlab Table 重建为 PDF
# 使用 macOS STHeiti 字体渲染中文,无需 Word/LibreOffice
# 完全本地处理,跨平台兼容

# ── 跳过无关文件 ────────────────────────────────────────
SKIP_NAMES = {'申请截图.png', 'ds_store', '.ds_store'}

def should_skip(fname):
    return fname.lower() in SKIP_NAMES or fname.startswith('.')

# ── 核心合并函数 ────────────────────────────────────────
def merge_print_package(trip_dir, out_path):
    """
    按规定顺序合并为打印材料包 PDF
    第1页:粘贴单(强制)
    第2页:研发经费使用单
    第3页起:交通票据(发票+行程单配对,火车票放最后)
    自动跳过:申请截图、.DS_Store 等无关文件
    """
    writer = PdfWriter()
    page = 1
    report_lines = []

    def add_page(src, label):
        nonlocal page
        fname = os.path.basename(src)
        reader = PdfReader(src if src.endswith('.pdf') else src)
        page_obj = reader.pages[0]
        # 票据 PDF 直接合并,打印机自动适配 A4(不做强制缩放,避免变形)
        if src.endswith('.pdf'):
            writer.add_page(page_obj)
        else:
            tmp = src + ".pdf"
            img_to_pdf(src, tmp)
            tmp_reader = PdfReader(tmp)
            tmp_page = tmp_reader.pages[0]
            writer.add_page(tmp_page)
            os.remove(tmp)
        report_lines.append(f"  第{page}页 [{label}] {fname}")
        page += 1

    # ── 第1页:粘贴单 ──────────────────────────────
    for pat in ["BEWG*.pdf", "截圖*.pdf", "截圖 *.pdf"]:
        files = glob.glob(os.path.join(trip_dir, "*粘贴单*", pat), recursive=True)
        files += glob.glob(os.path.join(trip_dir, "**", pat), recursive=True)
        files = [f for f in files if f.endswith('.pdf') and os.path.getsize(f) > 50000]
        if files:
            add_page(max(files, key=lambda f: os.path.getsize(f)), "粘贴单")
            break
    else:
        raise FileNotFoundError(f"❌ {trip_dir}: 粘贴单缺失")

    # ── 第2页:研发经费使用单 ──────────────────────
    docx_files = glob.glob(os.path.join(trip_dir, "*.docx"))
    for d in docx_files:
        if "研发经费使用单" in d:
            # 优先用已签字PDF版
            signed_pdf = d.replace(".docx", "_已签字.pdf")
            if os.path.exists(signed_pdf):
                add_page(signed_pdf, "表单-已签字")
            else:
                # 用 python-docx + reportlab 重建表单 PDF
                tmp = d.replace(".docx", "_rendered.pdf")
                try:
                    docx_to_pdf(d, tmp)
                    with open(tmp, 'rb') as f:
                        page_obj = PdfReader(f).pages[0]
                        writer.add_page(page_obj)
                        report_lines.append(f"  第{page}页 [表单-自动生成] {os.path.basename(tmp)}")
                        page += 1
                except Exception as e:
                    report_lines.append(f"  ⚠️ 表单 PDF 生成失败: {e}")
                    report_lines.append(f"     请在 Word 中打开表单另存为 PDF,命名: {os.path.basename(d.replace('.docx','_已签字.pdf'))}")
            break

    # ── 第3页起:交通票据 ──────────────────────────
    t_dir = os.path.join(trip_dir, "交通票据")
    if os.path.exists(t_dir):
        all_p = []
        for r, ds, fs in os.walk(t_dir):
            for f in fs:
                if should_skip(f):
                    continue
                full = os.path.join(r, f)
                if full.endswith('.pdf') or full.lower().endswith(('.png', '.jpg', '.jpeg')):
                    all_p.append(full)

        non_tr  = sorted([p for p in all_p if "火车票" not in p],
                         key=lambda p: os.path.getmtime(p))
        trains  = sorted([p for p in all_p if "火车票" in p],
                         key=lambda p: os.path.getmtime(p))

        for p in non_tr + trains:
            fname = os.path.basename(p)
            if "行程单" in fname:
                label = "行程单"
            elif "火车票" in fname:
                label = "火车票"
            else:
                label = "发票"
            add_page(p, label)

    with open(out_path, "wb") as f:
        writer.write(f)
    total_pages = page - 1
    report_lines.insert(0, f"\
{'='*50}")
    report_lines.insert(1, f"【{os.path.basename(trip_dir)}】打印材料包已生成(共 {total_pages} 页)")
    report_lines.append(f"  📄 {out_path}\
")
    return report_lines


def batch_merge_print_packages(reimburs_dir):
    """批量为所有行程生成打印材料包"""
    trip_dirs = sorted([d for d in os.listdir(reimburs_dir)
                        if d.startswith(("0","1","2")) and ("出差" in d or "公出" in d)])
    all_reports = []
    for trip in trip_dirs:
        trip_dir = os.path.join(reimburs_dir, trip)
        out = os.path.join(trip_dir, "打印材料包.pdf")
        print(f"\
{'='*50}")
        print(f"【{trip}】")
        try:
            lines = merge_print_package(trip_dir, out)
            all_reports.extend(lines)
        except FileNotFoundError as e:
            print(f"  {e}")
            all_reports.append(f"❌ {trip}: {e}")
        except Exception as e:
            print(f"  ⚠️ {e}")
            all_reports.append(f"⚠️ {trip}: {e}")
    return all_reports

执行后汇报:

╔══════════════════════════════════════════════════════╗
║              打印材料包生成完毕                     ║
╚══════════════════════════════════════════════════════╝

【01_3月2日杭州公出】共 4 页
  第1页 [粘贴单] BEWG-差旅报销单-粘贴单.pdf
  第2页 [表单-自动渲染] 研发经费使用单..._rendered.pdf
  第3页 [发票] 高德打车发票.pdf
  第4页 [行程单] 高德打车行程单.pdf

【02_3月11日无锡出差】共 10 页
  ...

⚠️ 请确认打印材料包内容正确。
确认后创建扫描打印文件文件夹(回复「确认」),
签字完成后回复「签字完成」。

Step 7.4 — 创建扫描打印文件文件夹

用户回复「确认」后执行:

import os, glob, shutil
from pypdf import PdfReader, PdfWriter

def create_scan_print_folder(trip_dir):
    """创建扫描打印文件文件夹,每页一个 PDF,顺序排放"""
    scan_dir = os.path.join(trip_dir, "扫描打印文件")
    os.makedirs(scan_dir, exist_ok=True)
    return scan_dir


def populate_scan_print_folder(trip_dir):
    """
    向扫描打印文件文件夹填充单页 PDF
    顺序:粘贴单 → 表单 → 发票+行程单 → 火车票
    """
    scan_dir = create_scan_print_folder(trip_dir)
    counter = [1]

    def num():
        n = counter[0]
        counter[0] += 1
        return f"0{n}" if n \x3C 10 else str(n)

    def copy_page(src, label):
        dst = os.path.join(scan_dir, f"{num()}_{label}.pdf")
        try:
            reader = PdfReader(src if src.endswith('.pdf') else src)
            writer = PdfWriter()
            writer.add_page(reader.pages[0])
            with open(dst, 'wb') as out:
                writer.write(out)
            print(f"  ✅ {os.path.basename(dst)}")
            return dst
        except Exception as e:
            print(f"  ⚠️ 复制失败 {src}: {e}")
            return None

    # 1. 粘贴单
    for pat in ["BEWG*.pdf", "截圖*.pdf"]:
        files = glob.glob(os.path.join(trip_dir, "*粘贴单*", pat), recursive=True)
        files += glob.glob(os.path.join(trip_dir, "**", pat), recursive=True)
        files = [f for f in files if f.endswith('.pdf') and os.path.getsize(f) > 50000]
        if files:
            copy_page(max(files, key=lambda f: os.path.getsize(f)), "粘贴单")
            break

    # 2. 表单
    for d in glob.glob(os.path.join(trip_dir, "*.docx")):
        if "研发经费使用单" in d:
            signed = d.replace(".docx", "_已签字.pdf")
            if os.path.exists(signed):
                copy_page(signed, "研发经费使用单_已签字")
            break

    # 3. 交通票据
    t_dir = os.path.join(trip_dir, "交通票据")
    if os.path.exists(t_dir):
        all_p = []
        for r, ds, fs in os.walk(t_dir):
            for f in fs:
                if f.lower() in ('申请截图.png', 'ds_store'):
                    continue
                full = os.path.join(r, f)
                if full.endswith('.pdf') or full.lower().endswith(('.png', '.jpg', '.jpeg')):
                    all_p.append(full)

        non_tr = sorted([p for p in all_p if "火车票" not in p],
                        key=lambda p: os.path.getmtime(p))
        trains = sorted([p for p in all_p if "火车票" in p],
                        key=lambda p: os.path.getmtime(p))

        for p in non_tr + trains:
            fname = os.path.basename(p)
            label = fname.replace(".pdf", "")
            copy_page(p, label)

    pages = counter[0] - 1
    print(f"\
  📁 扫描打印文件/ 已创建,共 {pages} 页")
    return scan_dir

汇报:

╔══════════════════════════════════════════════════════╗
║              扫描打印文件文件夹已创建                ║
╚══════════════════════════════════════════════════════╝

✅ 01_3月2日杭州公出:      扫描打印文件/(4页)
✅ 02_3月11日无锡出差:    扫描打印文件/(10页)
✅ 04_3月23日哈尔滨公出:  扫描打印文件/(9页)
✅ 05_3月30日北京公出:    扫描打印文件/(7页)
✅ 06_4月2日上海公出:      扫描打印文件/(7页)

⚠️ 下一步:
1. 将「领导签字用」文件夹中的 5 份表单拿去给领导签字
2. 签字后:将已签字表单扫描为 PDF
   放入对应行程的「扫描打印文件/」文件夹
   命名规则:02_研发经费使用单_已签字.pdf
3. 回复「签字完成」,AI 将更新最终版打印材料包

Step 8 — 签字完成收尾

用户回复「签字完成」后执行:

import os, glob, shutil
from pypdf import PdfMerger, PdfReader

def post_signature_update(trip_dir):
    """
    1. 找到签字版表单,放入扫描打印文件/
    2. 重新生成打印材料包(用签字版替换)
    """
    scan_dir = os.path.join(trip_dir, "扫描打印文件")
    os.makedirs(scan_dir, exist_ok=True)

    results = []

    # 1. 查找签字版表单
    signed_files = glob.glob(os.path.join(trip_dir, "**", "*已签字*.pdf"), recursive=True)
    if signed_files:
        sf = signed_files[0]
        dst = os.path.join(scan_dir, "02_研发经费使用单_已签字.pdf")
        shutil.copy2(sf, dst)
        results.append(f"✅ 已签字表单已归档: {os.path.basename(sf)}")
    else:
        results.append(f"⚠️ 未找到签字版表单,跳过")

    # 2. 重新生成打印材料包
    pkg = os.path.join(trip_dir, "打印材料包.pdf")
    try:
        # 清空旧包,重新生成(merge_print_package 会自动用已签字PDF)
        merge_print_package(trip_dir, pkg)
        results.append(f"✅ 打印材料包已更新为签字版")
    except Exception as e:
        results.append(f"⚠️ 更新打印材料包失败: {e}")

    return results


def batch_post_signature(reimburs_dir):
    """批量处理所有行程签字收尾"""
    trip_dirs = sorted([d for d in os.listdir(reimburs_dir)
                        if d.startswith(("0","1","2")) and ("出差" in d or "公出" in d)])
    all_results = []
    for trip in trip_dirs:
        trip_dir = os.path.join(reimburs_dir, trip)
        print(f"\
{'='*50}")
        print(f"【{trip}】")
        lines = post_signature_update(trip_dir)
        all_results.extend(lines)
        for l in lines:
            print(f"  {l}")
    return all_results

汇报:

╔══════════════════════════════════════════════════════╗
║              签字收尾完成                          ║
╚══════════════════════════════════════════════════════╝

✅ 01_3月2日杭州公出:      签字版已归档,打印材料包已更新
✅ 02_3月11日无锡出差:    签字版已归档,打印材料包已更新
✅ 04_3月23日哈尔滨公出:  签字版已归档,打印材料包已更新
✅ 05_3月30日北京公出:    签字版已归档,打印材料包已更新
✅ 06_4月2日上海公出:      签字版已归档,打印材料包已更新

全部报销材料整理完成。

报销文件夹路径

用途 路径
报销根目录 ~/报销/
原始资料备份 报销/00_原始资料/
领导签字用 报销/领导签字用/
打印材料包 报销/[行程]/打印材料包.pdf
扫描打印文件 报销/[行程]/扫描打印文件/
技能备份 报销/技能备份/expense_reimbursement_bak_YYYYMMDD_HHMMSS/

流程检查清单

归档阶段:

  • 申请截图已 OCR,已确认出差/公出类型
  • 专项项目已与用户确认
  • 模板已就位
  • 票据已递归扫描
  • 研发经费使用单已填写
  • 汇报后获得用户「确认」
  • 清理操作已获用户授权(删除/保留)

打印材料阶段:

  • Step 7.1 粘贴单核查已汇报
  • 获得用户「确认」(第一次停顿)
  • Step 7.3 打印材料包已生成,页数已汇报
  • 获得用户「确认」(第二次停顿)
  • Step 7.4 扫描打印文件文件夹已创建
  • 用户已拿去签字
  • 用户回复「签字完成」

收尾阶段:

  • Step 8 已执行,签字版已归档
  • 打印材料包 PDF 已更新为最终版
  • 最终完成汇报已输出

重要规则

  1. 原始文件保护 — 00_原始资料/ 始终保留,删除需明确授权
  2. 模板首次需提供 — 缺失时必须暂停,不能跳过
  3. 每步停顿确认 — Step 5.5、7.1、7.3、7.4、8 都是确认点,不说"确认"不继续
  4. 清理需单独授权 — 不默认删除任何文件
  5. 专项项目必须先确认 — 不自作主张
  6. 粘贴单永远第一页 — 财务要求,不可调整
  7. 火车票不配行程单 — 本身已含行程信息
  8. 自动跳过无关文件 — 申请截图、.DS_Store 等不进入打印包
  9. 签字版放入两个位置 — 扫描打印文件/ + 打印材料包.pdf 同时更新
安全使用建议
This skill appears to be what it claims: a local receipt-processing assistant that scans/unzips files, OCRs, classifies receipts, fills forms and produces merged PDFs. Before installing or running it: 1) Confirm the processing directory (REIMBURSEMENT_DIR) and check the script's default (SKILL docs reference ~/报销/ while the script defaults to ~/Desktop/报销/) so you know where files will be read/written. 2) Backup your reimbursement directory first — the skill can delete ZIPs and temp files if you authorize that. 3) Inspect any ZIPs you feed it (nested ZIPs can be extracted) and avoid running on untrusted archives to reduce risk of malicious payloads. 4) Install tesseract and Python deps from official sources; run the script in an isolated environment if you have concerns. 5) Note the skill creates a ~/memory summary file and new folders under your reimbursement directory — verify those locations if you need to restrict where data is stored. Overall there are no network endpoints, credentials, or unexplained behaviors in the package, but exercise normal caution when running file-processing scripts on your machine.
功能分析
Type: OpenClaw Skill Name: expense-reimbursement Version: 3.0.4 The skill automates travel expense reimbursement by performing recursive unzipping, XML parsing of receipts, and PDF merging. While its behavior aligns with the stated purpose, it employs high-risk operations such as recursive extraction of ZIP archives (unzip_and_parse.py) and XML parsing (parse_xml) without visible sanitization against ZipSlip or XXE vulnerabilities. The skill also requires broad file system access to the user's home directory (~/报销/) and handles sensitive financial documents. Although it includes multiple user-confirmation checkpoints in SKILL.md, the inherent risks associated with processing untrusted archives and XML data justify a suspicious classification.
能力评估
Purpose & Capability
Name/description (expense reimbursement, OCR, ZIP recursion, PDF merging, form filling) match the provided instructions and the included Python script. Declared dependencies (python-docx, pypdf, reportlab, Pillow, tesseract) are appropriate for the stated functionality.
Instruction Scope
Instructions and the script operate on user files (recursive scan of a reimbursement directory, unzip of ZIPs, XML parsing, OCR of images/PDFs) and write outputs (archived folders, merged PDFs, a summary file in ~/memory). The skill requires explicit user confirmations for major steps; it also offers an optional deletion step for original ZIPs/temps. These behaviors are coherent with the purpose but involve broad filesystem access and destructive actions only after user consent.
Install Mechanism
No install spec; skill is instruction-only with a small helper script. No remote downloads or archive extraction from untrusted network sources performed by the skill itself. The only install action a user would need is pip-installing legitimate Python packages and optionally installing tesseract OCR.
Credentials
The only optional environment variable referenced is REIMBURSEMENT_DIR to override the processing directory, which is appropriate. No credentials, tokens, or unrelated environment variables are requested.
Persistence & Privilege
The skill writes output files and creates directories under the chosen reimbursement directory and creates a summary file at ~/memory/YYYY-MM-DD_报销整理.md. always:false (not force-included). Writing local files is expected for this use case, but users should be aware of where files are created and of the optional deletion step.
如何使用
  1. 确保已安装 OpenClaw(本地或 Docker 部署)
  2. 在对话框中输入安装命令:/install expense-reimbursement
  3. 安装完成后,直接呼叫该 Skill 的名称或使用 /expense-reimbursement 触发
  4. 根据 Skill 的参数说明提供必要输入,即可获得结构化输出
版本历史
v3.0.4
v3.0.4: 表单PDF改用python-docx+reportlab Table重建(STHeiti字体);票据PDF直接合并(不强制A4);完整测试通过10页合并
v3.0.3
v3.0.3: 表单PDF改用AppleScript+Word自动导出(100%保真);移除票据scale_to强制缩放(由打印机自适应);新增osascript依赖
v3.0.2
v3.0.2: 表单改用用户从Word导出PDF;所有票据PDF强制scale_to(A4)统一尺寸;移除reportlab渲染;PdfWriter替代PdfMerger
v3.0.1
v3.0.1: 修复中文字体路径(STHeiti Light.ttc);测试通过:无锡出差10页合并成功;pypdf改用PdfWriter.merge()
v3.0.0
v3.0: 完整打印材料包生成流程(Step7-8);自动化PDF合并(Word图片PDF混排A4);分段确认机制;reportlab图片等比缩放渲染
v2.2.0
**Skill v2.2.0 (expense-reimbursement) changelog** - Added print package generation: automatically merges the expense cover sheet, filled forms, receipts, and itineraries into a single PDF, with the paste list always as the first page. - Updated workflow: standard process expanded from 7 to 8 steps to include print package merging. - Added new dependency: `pypdf` for PDF merging (see prerequisites/dependencies sections). - Updated description and documentation for enhanced PDF output and workflow changes. - No changes to user permission, data privacy, or local-only processing.
v1.1.4
Bilingual readability improved: cleaner visual separation using blockquotes for Chinese/English pairs, tables with dual headers, horizontal rules between major sections, full content preserved
v1.1.3
Improved readability: cleaner section spacing, horizontal rules, consistent hierarchy, grouped Chinese/English content, reduced redundant duplication
v1.1.2
Full bilingual rewrite: every section now has Chinese + English alternating throughout SKILL.md
v1.1.1
Bilingual description: Chinese/English alternating throughout ClawHub description field
v1.1.0
Added international platform support (Uber/Lyft/Grab/Bolt/Booking.com/Agoda/Airbnb), expanded city keyword library (20+ global cities), updated ClawHub description to English
v1.0.4
**Improved template detection for the expense reimbursement skill.** - Now automatically detects both blank and pre-filled templates (研发经费使用单) in the reimbursement folder, regardless of file name. - Prompts user to confirm if a non-standard or filled form should be used as template. - Gives clearer, user-facing instructions when no valid template is found. - Keeps original local-only processing and platform support unchanged. - Documentation updated to describe robust template detection and user prompting.
v1.0.3
- Enhanced template matching: Now automatically scans all `.docx` files in the directory and detects templates based on key phrases instead of fixed filenames, reducing risk of template loss or recognition failure. - Improved user guidance: If no template is found, clearly prompts user to provide any .docx template with instructions that filename is unrestricted. - Added user confirmation step: When a potential template is identified, asks user to confirm its suitability. - Optionally copies recognized template to a standard location for future re-use. - No changes to local-only processing, supported platforms, or privacy guarantees.
v1.0.2
- 新增首次使用需用户手动提供研发经费使用单模板(docx),缺失时主动检测并中断执行,提示补充模板后重试 - 不能在模板缺失时自动生成表单,确保报销规范性 - Step 1扫描票据时自动检测模板存在性,避免因模板缺失导致流程中断 - 文档细化了模板查找的路径规则,提升用户首次配置体验 - 明确强调“研发经费使用单_模板.docx”只需用户首次准备一次,后续自动复用
v1.0.1
expense-reimbursement v1.0.1 - Updated SKILL.md to clarify full local-only processing with no external uploads and no sensitive info hardcoded. - Added dependency and environment details (python-docx, tesseract, platform tools) and explicit platform support for macOS/Linux/Windows. - Improved description and workflow documentation to better reflect support for multiple platforms, full local security, and streamlined file scanning (including ZIP internals).
v1.0.0
Expense Reimbursement Skill v1.0.0 - Automates travel expense reimbursement document sorting, including recursive scanning of folders (and ZIP contents), bill type recognition, and OCR extraction for trip documents. - Determines trip type (business trip vs. public trip) based on application screenshots, and categorizes/archives documents by trip. - Fills out R&D expense forms using extracted trip and bill information, requiring user confirmation for special project numbers. - Supports PDF/OFD/XML formats from major Chinese platforms and handles additional screenshot-type documents for supplemental records. - Syncs completed logs with Obsidian if available; otherwise, outputs a conversation summary for archival. - Always preserves original files in a backup directory; deletion requires explicit user permission.
元数据
Slug expense-reimbursement
版本 3.0.4
许可证 MIT-0
累计安装 0
当前安装数 0
历史版本数 16
常见问题

Expense Reimbursement 是什么?

差旅报销票据整理 / Travel Expense Reimbursement:支持高德/Didi/滴滴/Uber打车、12306火车票、机票、酒店住宿;递归扫描含ZIP内部;OCR识别行程单;判断出差/公出类型;按行程自动归档;填写研发经费使用单;生成打印材料包PDF(粘贴单永远首页);自动化PDF合并(图片+... 它是一个面向 Claude Code / OpenClaw 的 AI Agent Skill 插件,目前累计下载 125 次。

如何安装 Expense Reimbursement?

在 OpenClaw 或 Claude Code 对话框中运行命令「/install expense-reimbursement」即可一键安装,无需额外配置。

Expense Reimbursement 是免费的吗?

是的,Expense Reimbursement 完全免费,采用 MIT-0 许可证,可自由下载、安装和使用。

Expense Reimbursement 支持哪些平台?

Expense Reimbursement 跨平台运行,可在任意部署了 OpenClaw / Claude Code 的环境中使用(cross-platform)。

谁开发了 Expense Reimbursement?

由 LorwaLeroy(@lorwaleroy)开发并维护,当前版本 v3.0.4。

💬 留言讨论