本 skill 做这件事:把用户上月的 12306/南航/滴滴电子发票从邮箱抓下来,按项目规则自动归属,按甲方拆 sheet 填进项目费用报销表。涉及的项目规则(地点/抬头/报销方/项目组成员)在 _config/projects.yaml 里配置化。
本 skill 不做:开发票、改发票、提交报销到公司系统、计算个人所得税。
本 skill 的所有脚本和模板位于:
SKILL_ROOT = C:\Users\Lenovo\.workbuddy\skills\moways-expense-reimbursement
SKILL_ROOT/scripts/ — Python 脚本
SKILL_ROOT/templates/ — 配置模板
重要:在后续指令中,$SKILL_ROOT 均指此路径。
使用 WorkBuddy 管理的 Python 环境:
PYTHON=C:\Users\Lenovo\.workbuddy\binaries\python\envs\default\Scripts\python.exe
如果虚拟环境尚未创建,先执行:
C:\Users\Lenovo\.workbuddy\binaries\python\versions\3.13.12\python.exe -m venv C:\Users\Lenovo\.workbuddy\binaries\python\envs\default
依赖安装:
C:\Users\Lenovo\.workbuddy\binaries\python\envs\default\Scripts\pip.exe install pdfplumber pypdf openpyxl ruamel.yaml pyyaml reportlab
Windows 编码注意:所有脚本运行时需设置环境变量 PYTHONIOENCODING=utf-8,否则含 emoji 的 print 输出会触发 GBK 编码错误。
每次触发都先确定工作区路径(记为 $WS)。优先级:
_config/ 子目录,那就是 $WS
定下 $WS 后,后续所有脚本调用都传 EXPENSE_WORKSPACE=$WS。
判断依据:$WS/_config/mailbox.yaml 是否存在。
如果不存在 → 进入 Setup Mode。
如果存在 → 进入 Monthly Mode。
按顺序完成以下步骤,每一步用 AskUserQuestion 或清晰的提示收集信息。
把 skill 自带的模板文件复制到工作区:
$SKILL_ROOT/templates/mailbox.yaml → $WS/_config/mailbox.yaml
$SKILL_ROOT/templates/projects.yaml → $WS/_config/projects.yaml
> Excel 模板说明:项目费用报销表由 fill_excel.py 在运行时自动生成(列结构、A1标题、合计行等),无需预置 .xlsx 模板文件。
同时 mkdir 出 $WS/_收件箱/、$WS/_backups/、$WS/_报销包/。
两轮交互:第一轮让用户输入所有邮箱地址,第二轮逐个邮箱问授权码。
用 AskUserQuestion 问(至少2个选项,用户通过"其他"自由输入邮箱地址):
> 你的邮箱是?(可以填写多个,用逗号分隔,请点击"其他"输入)
选项示例:我来填写 / 还没有邮箱(用户实际通过"其他"输入邮箱地址)
用户输入后,解析出所有邮箱地址,根据域名自动推断 IMAP 服务器和 needs_id_command:
对第一轮收集到的每个邮箱,逐个用 AskUserQuestion 问授权码,一个问完再问下一个:
> 请提供 {邮箱地址} 的 IMAP 授权码(不是邮箱登录密码!需要在邮箱网页版「设置 → POP3/IMAP/SMTP」里单独生成)。
> ⚠️ 授权码会明文写入 mailbox.yaml;如果不放心,可以先把配置文件移到非云同步目录。
每个邮箱收集完授权码后,立即改写 $WS/_config/mailbox.yaml 里对应账号的 host / port / user / auth_code / needs_id_command 字段,并把 enabled: true。未配置的账号保留 enabled: false。
问用户:
> 你手头正在跑的项目有哪些?我需要知道每个项目的:(1)项目简称;(2)甲方抬头全称;(3)主要出差地点关键词(如"厦门"、"华南新材料创新园");(4)大小交通各由谁报销(甲方 / 博维 / 个人);(5)项目组成员清单(用于自动填"使用人员"列)。
用 AskUserQuestion 逐个项目收集。每个项目追问:
KERUIXIN)
科瑞信)
张三,张三,李四) ← v0.4 必填
收集完后 改写 $WS/_config/projects.yaml:删掉三个 EXAMPLE 项目,用用户提供的项目填进去,每个项目都要写 team_members 数组。attribution_rules 和 header_validation 部分保留不动。
把写好的 mailbox.yaml 和 projects.yaml 关键内容向用户复述一遍(隐藏授权码只显示 **),让他确认无误。确认后提示:
> 配置已保存到 _config/ 目录。现在说一句「帮我处理上月报销」就能开始正常流程。
博维差旅费报销有两种业务节奏,必须先识别用户意图:
package_for_reimbursement.py --filter-side 博维
package_for_reimbursement.py --filter-project CODE --filter-side 甲方
判断优先级:提了具体项目名 → B;提了"博维"或"财务" → A;其他 → C。
遇到歧义用 AskUserQuestion 让用户二选一。
每次进入 Monthly Mode 都必须先用 AskUserQuestion 一次性收集这三项,再开始拉邮件:
问题1:本次报销的经办人是谁?
- 默认:张三
- 如果是其他同事经办,让用户填写
问题2:本项目本次报销涉及人员是谁?
- 默认:从 projects.yaml 该项目的 team_members 字段把全员展开
- 让用户确认或改写为本次实际涉及的人员
- 如果是模式 B(单项目),只问该项目
- 如果是模式 A/C(多项目),逐个项目问,或允许用户对所有项目用同一组成员
问题3(v0.4.3 新增):本次报销有什么特殊的归属说明吗?
- 默认:无(让 process_reimbursement.py 自动按抬头/目的地/接驳规则判定)
- 例 1:"4 月 12 日去厦门那趟全部归科瑞信"
- 例 2:"郑州的几张票如果归不到,归幸福宜居"
- 例 3:"5 月 7 日的南航全电发票是去厦门,归科瑞信"
- 例 4:"这次没有特殊情况"
把答案存成三个变量备用:
$OPERATOR(一个字符串,如"张三")
$USERS_JSON(JSON 字符串,形如 {"KERUIXIN":"张三、李四"};所有项目共用一组人员可用 {"_default_":"张三、张三"})
$ATTRIBUTION_HINT(用户对归属的特殊说明,文本;为空表示完全靠自动归属)
注意:
team_members 字段(升级前老配置),立刻提示用户补充。
$ATTRIBUTION_HINT 用法见 Step 3.5。
计算出 $DATE_FROM 和 $DATE_TO(ISO 格式,YYYY-MM-DD)。
$PYTHON "$SKILL_ROOT/scripts/fetch_invoices.py" \
--since $DATE_FROM --until $DATE_TO
需设置环境变量 EXPENSE_WORKSPACE=$WS。
脚本产出:$WS/_收件箱/YYYY-MM/ 下的 PDF 附件和 manifest.csv。
如果某个账号拉取失败(授权码过期等),提示用户去邮箱后台重新生成授权码,并询问是否要更新 mailbox.yaml。
EXPENSE_WORKSPACE=$WS $PYTHON "$SKILL_ROOT/scripts/process_reimbursement.py" \
--trip-dir _收件箱/YYYY-MM \
--date-from $DATE_FROM --date-to $DATE_TO
产出:
_收件箱/YYYY-MM/03_归属预览.csv
_收件箱/YYYY-MM/99_历史票.csv
_收件箱/YYYY-MM/99_待人工确认.csv
EXPENSE_WORKSPACE=$WS $PYTHON "$SKILL_ROOT/scripts/parse_airline.py" \
--trip-dir _收件箱/YYYY-MM \
--date-from $DATE_FROM --date-to $DATE_TO
脚本行为:
_收件箱/YYYY-MM/机票/.pdf 和 _收件箱/YYYY-MM/南航/.pdf
'机票',carrier 字段填承运商中文名
03_归属预览.csv
如果目录不存在或为空,脚本静默跳过。这一步总是要跑,跑了无害。
EXPENSE_WORKSPACE=$WS $PYTHON "$SKILL_ROOT/scripts/parse_hotel.py" \
--trip-dir _收件箱/YYYY-MM \
--date-from $DATE_FROM --date-to $DATE_TO
脚本行为:
_收件箱/YYYY-MM/酒店/*.pdf,配对发票 + 结账单
如果目录不存在或为空,脚本静默跳过。
EXPENSE_WORKSPACE=$WS $PYTHON "$SKILL_ROOT/scripts/parse_meal.py" \
--trip-dir _收件箱/YYYY-MM \
--date-from $DATE_FROM --date-to $DATE_TO
行为同 parse_hotel.py:扫 _收件箱/YYYY-MM/餐饮/*.pdf,按抬头/销方匹配自动归属,
source='餐饮',fill_excel.py 看到会自动写"科目=餐饮"。
如果目录不存在或为空,脚本静默跳过。
合并所有解析器之后的 99_待人工确认.csv,按下面顺序处理(v0.4.3 增强):
$ATTRIBUTION_HINT 自动消化:把每条 ambiguous 票的『日期/起终点/金额/抬头』和 $ATTRIBUTION_HINT 一起读,能匹配到具体项目的就直接写 project_code 进 03_归属预览.csv,不再问用户。
AskUserQuestion 让他选 project_code。
在以下两种情况下,必须主动触发新项目登记子流程:
情况 1(事前):用户在触发消息里提到某个项目名,但 projects.yaml 里不存在。
情况 2(事后):解析完发票后,99_待人工确认.csv 里出现的陌生买方抬头不匹配任何已知项目的 invoice_headers。
收集:
张三,张三,李四) ← v0.4 必填
EXPENSE_WORKSPACE=$WS $PYTHON "$SKILL_ROOT/scripts/add_project.py" \
--code YUANDA_GUANGRE \
--name "远大光热" \
--client-name "远大光热科技股份有限公司" \
--invoice-headers "远大光热,远大" \
--locations-primary "长沙,岳麓" \
--big-transport 甲方 --small-transport 博维 \
--trigger-mode hybrid \
--team-members "张三,张三,李四" \
--notes "..."
追加后必须重跑 Step 3。
EXPENSE_WORKSPACE=$WS $PYTHON "$SKILL_ROOT/scripts/fill_excel.py" \
--preview _收件箱/YYYY-MM/03_归属预览.csv \
--date-from $DATE_FROM \
--date-to $DATE_TO \
--operator "$OPERATOR" \
--users-json "$USERS_JSON"
脚本行为(v0.4.9):
{项目名}项目费用报销表(YYYY年M月D日-YYYY年M月D日)
A 日期 / B 星期 / C 类别 / D 金额 / E 经办人 / F 使用人员 / G 描述 / H 是否有发票
--operator 传入的值
--users-json 中该项目的值
_backups/
模式 A(博维月度):只出博维 zip
EXPENSE_WORKSPACE=$WS $PYTHON "$SKILL_ROOT/scripts/package_for_reimbursement.py" \
--preview _收件箱/YYYY-MM/03_归属预览.csv \
--month-tag "M月" \
--filter-side 博维
模式 B(甲方即时):只出某项目甲方 zip
EXPENSE_WORKSPACE=$WS $PYTHON "$SKILL_ROOT/scripts/package_for_reimbursement.py" \
--preview _收件箱/YYYY-MM/03_归属预览.csv \
--month-tag "4月8-15日" \
--filter-project KERUIXIN \
--filter-side 甲方
模式 C(月度全量):不加过滤
EXPENSE_WORKSPACE=$WS $PYTHON "$SKILL_ROOT/scripts/package_for_reimbursement.py" \
--preview _收件箱/YYYY-MM/03_归属预览.csv \
--month-tag "M月"
输出在 $WS/_报销包/YYYY-MM/ 下。
向用户报告:
归属优先级简表:
$ATTRIBUTION_HINT 用于覆盖与兜底
禁忌操作:
--keep-other-sheets
| 错误现象 | 处理 |
|---|---|
| BadZipFile | 从 _backups/ 拷最近一份回来 |
| IMAP 登录失败 | 告诉用户授权码可能过期或被吊销,引导他去邮箱后台重新生成;拿到新码后更新 mailbox.yaml 的 auth_code 字段 |
| pdfplumber 提取空字符串 | 该 PDF 可能是扫描件/图片版,跳过并记录到 99_待人工确认.csv |
| 时间窗外票据 | 脚本已自动隔离到 99_历史票.csv |
| projects.yaml 没有 team_members | 提示用户补充,可手填本次使用人员后回写 yaml |
用户在新工作区首次触发时,先给一段简短欢迎:
> 我发现这是你第一次用这个报销工具(_config/ 还没创建)。下面我会用几个问题收集你的邮箱、项目规则和项目组成员,大概需要 3-5 分钟。配置好以后,每个月只需要说一句「处理上月报销」,我会再问你三件事——本次经办人、本次涉及人员、有没有特殊归属说明,然后就能一键搞定。准备好了吗?
然后进入 Setup Mode 的 Step 1。
发票买方抬头命中某项目的 client.invoice_headers → 归该项目,置信度 1.0。
适用于高铁无抬头、抬头开错、滴滴行程单等场景。按优先级:
didi_catch_all
Step 0 收集的 $ATTRIBUTION_HINT,用于修正、兜底和用户主动覆盖。
共 1 个版本