下载地址
https://www.zhengdianxue.com/wp-content/uploads/skills/org-vc3t81nj--moways-expense-reimbursement-moways.zip将提示词发送给你的 AI 安装该 skills
请根据 https://skillhub.cn/install/skillhub.md,安装 @org-vc3t81nj/moways-expense-reimbursement-moways。博维管理咨询差旅报销自动化:从邮箱拉取电子发票,按项目归属分类,填入报销表单,支持按甲方拆分打印。一键完成月度报销整理。
阅读全文 (展开查看完整说明)
概述
博维差旅报销 Skill
职责边界
本 skill 做这件事:把用户上月的 12306/南航/滴滴电子发票从邮箱抓下来,按项目规则自动归属,按甲方拆 sheet 填进项目费用报销表。涉及的项目规则(地点/抬头/报销方/项目组成员)在 _config/projects.yaml 里配置化。
本 skill 不做:开发票、改发票、提交报销到公司系统、计算个人所得税。
Skill 根目录
本 skill 的所有脚本和模板位于:
SKILL_ROOT = C:UsersLenovo.workbuddyskillsmoways-expense-reimbursementSKILL_ROOT/scripts/— Python 脚本SKILL_ROOT/templates/— 配置模板
重要:在后续指令中,$SKILL_ROOT 均指此路径。
Python 运行环境
使用 WorkBuddy 管理的 Python 环境:
PYTHON=C:UsersLenovo.workbuddybinariespythonenvsdefaultScriptspython.exe如果虚拟环境尚未创建,先执行:
C:UsersLenovo.workbuddybinariespythonversions3.13.12python.exe -m venv C:UsersLenovo.workbuddybinariespythonenvsdefault依赖安装:
C:UsersLenovo.workbuddybinariespythonenvsdefaultScriptspip.exe install pdfplumber pypdf openpyxl ruamel.yaml pyyaml reportlabWindows 编码注意:所有脚本运行时需设置环境变量 PYTHONIOENCODING=utf-8,否则含 emoji 的 print 输出会触发 GBK 编码错误。
工作区判定
每次触发都先确定工作区路径(记为 $WS)。优先级:
- 若当前会话工作目录包含
_config/子目录,那就是$WS - 否则若用户消息里提到具体路径,用那个路径
- 否则让用户选择一个空文件夹或已有的报销文件夹作为工作区
定下 $WS 后,后续所有脚本调用都传 EXPENSE_WORKSPACE=$WS。
首次运行检测(关键!)
判断依据:$WS/_config/mailbox.yaml 是否存在。
如果不存在 → 进入 Setup Mode。如果存在 → 进入 Monthly Mode。
Setup Mode — 首次运行引导
按顺序完成以下步骤,每一步用 AskUserQuestion 或清晰的提示收集信息。
Step 1:把模板拷进工作区
把 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/_报销包/。
Step 2:引导填写邮箱
两轮交互:第一轮让用户输入所有邮箱地址,第二轮逐个邮箱问授权码。
第一轮:邮箱地址
用 AskUserQuestion 问(至少2个选项,用户通过"其他"自由输入邮箱地址):
你的邮箱是?(可以填写多个,用逗号分隔,请点击"其他"输入)
选项示例:我来填写 / 还没有邮箱(用户实际通过"其他"输入邮箱地址)
用户输入后,解析出所有邮箱地址,根据域名自动推断 IMAP 服务器和 needs_id_command:
- 126/163 → imap.126.com / imap.163.com,needs_id_command: true
- QQ → imap.qq.com,needs_id_command: false
- 腾讯企业邮箱 → imap.exmail.qq.com,needs_id_command: false
- 阿里云企业邮箱 → imap.mxhichina.com,needs_id_command: false
- Gmail → imap.gmail.com,needs_id_command: false
- 无法识别 → 追问 IMAP 服务器地址
第二轮:逐个邮箱问授权码
对第一轮收集到的每个邮箱,逐个用 AskUserQuestion 问授权码,一个问完再问下一个:
请提供 {邮箱地址} 的 IMAP 授权码(不是邮箱登录密码!需要在邮箱网页版「设置 → POP3/IMAP/SMTP」里单独生成)。
⚠️ 授权码会明文写入 mailbox.yaml;如果不放心,可以先把配置文件移到非云同步目录。
每个邮箱收集完授权码后,立即改写 $WS/_config/mailbox.yaml 里对应账号的 host / port / user / auth_code / needs_id_command 字段,并把 enabled: true。未配置的账号保留 enabled: false。
Step 3:引导填写项目(v0.4 增加项目组成员)
问用户:
你手头正在跑的项目有哪些?我需要知道每个项目的:(1)项目简称;(2)甲方抬头全称;(3)主要出差地点关键词(如"厦门"、"华南新材料创新园");(4)大小交通各由谁报销(甲方 / 博维 / 个人);(5)项目组成员清单(用于自动填"使用人员"列)。
用 AskUserQuestion 逐个项目收集。每个项目追问:
- 代号(英文大写,如
KERUIXIN) - 项目简称(中文,如
科瑞信) - 甲方抬头关键词(用于 OCR 命中)
- 主要地点关键词(primary locations)
- 次要地点关键词(secondary,可选)
- 是否有「该区域打车一律归此项目」的硬规则(didi_catch_all,可选)
- 大交通报销方(甲方 / 博维)
- 小交通报销方(甲方 / 博维)
- 项目组成员(逗号分隔,如
张三,张三,李四) ← v0.4 必填
收集完后 改写 $WS/_config/projects.yaml:删掉三个 EXAMPLE 项目,用用户提供的项目填进去,每个项目都要写 team_members 数组。attribution_rules 和 header_validation 部分保留不动。
Step 4:确认配置
把写好的 mailbox.yaml 和 projects.yaml 关键内容向用户复述一遍(隐藏授权码只显示 ****),让他确认无误。确认后提示:
配置已保存到
_config/目录。现在说一句「帮我处理上月报销」就能开始正常流程。
工作模式识别(关键!先分清跑哪种再执行)
博维差旅费报销有两种业务节奏,必须先识别用户意图:
模式 A:博维月度报销(每月 10 日前,刚性截止)
- 触发词:「处理上月博维报销」「把上月要交财务的整理一下」「4 月博维那部分」
- 范围:上一个自然月 + 所有项目 + side=博维 的票
- 动作:拉邮件 → 解析 → 填项目费用报销表 → 打博维 zip
- 脚本参数:
package_for_reimbursement.py --filter-side 博维
模式 B:甲方项目即时报销(出差回来就做)
- 触发词:「帮我完成科瑞信项目的甲方报销」「把上周宣城那趟的甲方发票整给琥崧」「出差回来了,整理甲方发票」
- 范围:最近一段时间(默认 14 天)+ 指定项目 + side=甲方 的票
- 动作:拉邮件 → 解析 → 填项目费用报销表 → 打甲方 zip
- 脚本参数:
package_for_reimbursement.py --filter-project CODE --filter-side 甲方
模式 C:月度全量(8 号前一把梭,可选)
- 触发词:「帮我处理本月报销」「把上月所有发票都整理出来」
- 范围:上一自然月全部票
- 动作:五步全跑,生成所有 zip + 填 Excel
- 脚本参数:不加过滤
判断优先级:提了具体项目名 → B;提了"博维"或"财务" → A;其他 → C。遇到歧义用 AskUserQuestion 让用户二选一。
Monthly Mode — 月度/即时流程
Step 0:触发即询问『经办人 + 使用人员 + 特殊归属说明』(v0.4.3 关键步骤)
每次进入 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(用户对归属的特殊说明,文本;为空表示完全靠自动归属)
注意:
- 如果 projects.yaml 还没有
team_members字段(升级前老配置),立刻提示用户补充。 $ATTRIBUTION_HINT用法见 Step 3.5。
Step 1:确定报销窗口
- 模式 A / C:处理上一个自然月(1 号到月底)。用户若指定了具体月份,按用户说的。
- 模式 B:默认拉最近 14 天。用户若说了具体出差日期,按用户说的。
计算出 $DATE_FROM 和 $DATE_TO(ISO 格式,YYYY-MM-DD)。
Step 2:拉邮件
$PYTHON "$SKILL_ROOT/scripts/fetch_invoices.py"
--since $DATE_FROM --until $DATE_TO需设置环境变量 EXPENSE_WORKSPACE=$WS。
脚本产出:$WS/_收件箱/YYYY-MM/ 下的 PDF 附件和 manifest.csv。
如果某个账号拉取失败(授权码过期等),提示用户去邮箱后台重新生成授权码,并询问是否要更新 mailbox.yaml。
Step 3:解析 + 归属
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
Step 3.4:补解析航空发票(v0.4.7 通用航空)
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 - 按航班号前缀识别承运商
- source 统一为
'机票',carrier 字段填承运商中文名 - 按抬头命中 + 起终点地点匹配自动归属
- 时间窗内的票 append 到
03_归属预览.csv
如果目录不存在或为空,脚本静默跳过。这一步总是要跑,跑了无害。
Step 3.42:补解析酒店发票(v0.4.5 新增)
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,配对发票 + 结账单 - 提取:发票号、入住日、离店日、酒店名、买方抬头、价税合计
- 按抬头命中 + 酒店名/地点匹配自动归属
- source='酒店',fill_excel.py 看到会写"科目=住宿"
如果目录不存在或为空,脚本静默跳过。
Step 3.43:补解析餐饮发票(v0.4.6 新增)
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 看到会自动写"科目=餐饮"。
如果目录不存在或为空,脚本静默跳过。
Step 3.5:检查待人工确认清单
合并所有解析器之后的 99_待人工确认.csv,按下面顺序处理(v0.4.3 增强):
- 先用 Step 0 收集的
$ATTRIBUTION_HINT自动消化:把每条 ambiguous 票的『日期/起终点/金额/抬头』和$ATTRIBUTION_HINT一起读,能匹配到具体项目的就直接写project_code进 03_归属预览.csv,不再问用户。 - hint 消化不掉的剩余票:再展示给用户,逐条用
AskUserQuestion让他选project_code。 - 处理完后重新写一次 03_归属预览.csv,把 hint/手动归属的票合进来。
Step 3.5:新项目识别与登记
在以下两种情况下,必须主动触发新项目登记子流程:
情况 1(事前):用户在触发消息里提到某个项目名,但 projects.yaml 里不存在。情况 2(事后):解析完发票后,99_待人工确认.csv 里出现的陌生买方抬头不匹配任何已知项目的 invoice_headers。
登记子流程(用 AskUserQuestion)
收集:
- 项目代号(英文大写)
- 项目简称(中文)
- 甲方全称
- 甲方抬头识别词(逗号分隔)
- 主要出差地点关键词(逗号分隔)
- 次要地点关键词(可选)
- 滴滴硬规则关键词(可选)
- 大交通报销方(甲方 / 博维)
- 小交通报销方(甲方 / 博维)
- 触发模式(per_trip / monthly / hybrid)
- 项目组成员(逗号分隔,如
张三,张三,李四) ← v0.4 必填 - 备注(可选)
追加到 projects.yaml
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。
Step 4:填项目费用报销表(v0.4 新参数)
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):
- 默认 workbook 文件名带日期段 + 涉及项目
- A1 标题写为
{项目名}项目费用报销表(YYYY年M月D日-YYYY年M月D日) - 列结构:
A 日期 / B 星期 / C 类别 / D 金额 / E 经办人 / F 使用人员 / G 描述 / H 是否有发票 - E 列经办人 =
--operator传入的值 - F 列使用人员 = 优先用
--users-json中该项目的值 - 末尾自动加合计行(D 列 SUM 公式)
- 写完后默认删除所有非本次创建的 sheets
- 旧表自动备份到
_backups/
Step 5:按报销方打包发票
模式 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/ 下。
Step 6:汇报结果
向用户报告:
- 本次总共处理了多少张票、总金额多少
- 每个项目分别多少张、多少钱、经办人是谁、使用人员是谁
- 生成了哪几个报销包(甲方 zip 发谁、博维 zip 自己打印)
- 待人工确认的票(如果有)
- 项目费用报销表文件位置
- 各个报销包 zip 的路径
业务规则备忘
归属优先级简表:
- 抬头优先——发票买方抬头命中项目 invoice_headers → 直接归属(置信度 1.0,最强证据)
- 地点 fallback——抬头缺失或开错时按起点/终点匹配
- 用户 hint——Step 0 收集的
$ATTRIBUTION_HINT用于覆盖与兜底
禁忌操作:
- 不要把用户的 mailbox.yaml 内容贴到对话里
- v0.4 起 fill_excel.py 默认会清掉非本次的 sheets——如果用户希望保留历史 sheet,加
--keep-other-sheets - 不要因为某张票"地点像 X 项目"就改归属——抬头明确写了 Y 项目就该归 Y
错误恢复
| 错误现象 | 处理 |
|---|---|
BadZipFile | 从 _backups/ 拷最近一份回来 |
| IMAP 登录失败 | 告诉用户授权码可能过期或被吊销,引导他去邮箱后台重新生成;拿到新码后更新 mailbox.yaml 的 auth_code 字段 |
| pdfplumber 提取空字符串 | 该 PDF 可能是扫描件/图片版,跳过并记录到 99_待人工确认.csv |
| 时间窗外票据 | 脚本已自动隔离到 99_历史票.csv |
| projects.yaml 没有 team_members | 提示用户补充,可手填本次使用人员后回写 yaml |
首次 hand-off 话术
用户在新工作区首次触发时,先给一段简短欢迎:
我发现这是你第一次用这个报销工具(
_config/还没创建)。下面我会用几个问题收集你的邮箱、项目规则和项目组成员,大概需要 3-5 分钟。配置好以后,每个月只需要说一句「处理上月报销」,我会再问你三件事——本次经办人、本次涉及人员、有没有特殊归属说明,然后就能一键搞定。准备好了吗?
然后进入 Setup Mode 的 Step 1。
自动归属能力说明(用户问到时引用)
第一原则:抬头优先(最强证据)
发票买方抬头命中某项目的 client.invoice_headers → 归该项目,置信度 1.0。
第二原则:地点匹配(抬头不可用时的 fallback)
适用于高铁无抬头、抬头开错、滴滴行程单等场景。按优先级:
- 滴滴硬规则
didi_catch_all - 跨项目段:大交通 A→B 起终点分别命中不同项目 → 归目的项目
- 滴滴接驳:起/终点含「站/机场」→ 找同日最近一张大交通票继承
- 基础地点匹配
第三原则:用户 hint(覆盖与兜底)
Step 0 收集的 $ATTRIBUTION_HINT,用于修正、兜底和用户主动覆盖。