@rokoai/fapiao 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +60 -0
- package/bin/install.js +36 -0
- package/package.json +32 -0
- package/skill/SKILL.md +219 -0
- package/skill/agents/openai.yaml +7 -0
- package/skill/scripts/ensure_month.py +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 rokoai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# 发票整理 Skill
|
|
2
|
+
|
|
3
|
+
这是一个给 Codex 使用的发票整理 skill,用来把本地发票 PDF、服务商 invoice/receipt、Gmail 邮件里的发票附件,整理成按月份归档的文件夹和 CSV 台账。
|
|
4
|
+
|
|
5
|
+
它适合处理这些场景:
|
|
6
|
+
|
|
7
|
+
- 整理中国电子发票 PDF
|
|
8
|
+
- 整理 ChatGPT、云服务、软件订阅等海外服务的 invoice 和 receipt
|
|
9
|
+
- 从 Gmail 搜索近期发票邮件并下载附件
|
|
10
|
+
- 按月份创建 `YYYY-MM/` 文件夹和 `YYYY_M.csv` 台账
|
|
11
|
+
- 按规则重命名发票文件
|
|
12
|
+
- 去重重复发票或重复下载的邮件附件
|
|
13
|
+
- 写入统一 CSV 字段:日期、类型、金额、支付账户、用途、分类、是否报销、发票、备注
|
|
14
|
+
|
|
15
|
+
## 安装
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx @rokoai/fapiao
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
安装后会把 skill 复制到:
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
~/.codex/skills/fapiao
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
之后在 Codex 里这样调用:
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
$fapiao
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
例如:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
$fapiao 整理这个月的发票,更新 CSV 台账
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 工作方式
|
|
40
|
+
|
|
41
|
+
这个 skill 会把当前工作目录当成发票归档根目录,不依赖任何固定的本地路径。默认结构是:
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
2026-05/
|
|
45
|
+
2026_5.csv
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
CSV 表头固定为:
|
|
49
|
+
|
|
50
|
+
```csv
|
|
51
|
+
日期,类型,金额,支付账户,用途,分类,是否报销,发票,备注
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
归档文件名示例:
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
2026-05-09+加油+362.00.pdf
|
|
58
|
+
2026-05-16+ChatGPT Plus订阅+138.00+invoice.pdf
|
|
59
|
+
2026-05-16+ChatGPT Plus订阅+138.00+receipt.pdf
|
|
60
|
+
```
|
package/bin/install.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
10
|
+
const source = path.join(packageRoot, "skill");
|
|
11
|
+
const destination = path.join(os.homedir(), ".codex", "skills", "fapiao");
|
|
12
|
+
|
|
13
|
+
function copyDirectory(src, dest) {
|
|
14
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
15
|
+
|
|
16
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
17
|
+
const srcPath = path.join(src, entry.name);
|
|
18
|
+
const destPath = path.join(dest, entry.name);
|
|
19
|
+
|
|
20
|
+
if (entry.isDirectory()) {
|
|
21
|
+
copyDirectory(srcPath, destPath);
|
|
22
|
+
} else if (entry.isFile()) {
|
|
23
|
+
fs.copyFileSync(srcPath, destPath);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!fs.existsSync(source)) {
|
|
29
|
+
console.error(`Missing bundled skill directory: ${source}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
copyDirectory(source, destination);
|
|
34
|
+
|
|
35
|
+
console.log(`Installed fapiao skill to ${destination}`);
|
|
36
|
+
console.log("Invoke it in Codex with: $fapiao");
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rokoai/fapiao",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Codex skill for organizing invoice PDFs, Gmail invoice emails, and monthly CSV ledgers.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"fapiao": "./bin/install.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"skill",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"codex",
|
|
16
|
+
"skill",
|
|
17
|
+
"invoice",
|
|
18
|
+
"fapiao",
|
|
19
|
+
"gmail"
|
|
20
|
+
],
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/RokoSkills/fapiao.git"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fapiao
|
|
3
|
+
description: Organize Chinese and service-provider invoice PDFs, receipts, Gmail invoice emails, monthly invoice folders, and root-level monthly CSV ledgers from the current working directory. Use when the user asks to整理发票, process new invoice PDFs, search Gmail for recent invoice emails, rename invoice/receipt files, write or audit monthly CSV rows, deduplicate invoices, or update an invoice archive.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Fapiao
|
|
7
|
+
|
|
8
|
+
Manage an invoice archive rooted at the current working directory. Treat the current directory as the archive root; never hardcode or depend on a user's private absolute paths.
|
|
9
|
+
|
|
10
|
+
## Archive Layout
|
|
11
|
+
|
|
12
|
+
Use one folder per month:
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
YYYY-MM/
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Use one root-level CSV per month, not inside the month folder:
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
YYYY_M.csv
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
2026-05/
|
|
28
|
+
2026_5.csv
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
CSV header:
|
|
32
|
+
|
|
33
|
+
```csv
|
|
34
|
+
日期,类型,金额,支付账户,用途,分类,是否报销,发票,备注
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
When preparing a month, run:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
python3 scripts/ensure_month.py 2026 6
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
If `scripts/ensure_month.py` is missing in the archive, create the month folder and CSV manually with the fixed header.
|
|
44
|
+
|
|
45
|
+
## Workflow
|
|
46
|
+
|
|
47
|
+
1. Confirm the target year and month from the user's request or current date.
|
|
48
|
+
2. Check the local archive state first. If the target month folder or root monthly CSV is missing, create it.
|
|
49
|
+
3. On macOS, inspect `~/Downloads` for invoice-related PDFs created or modified in the last 7 days. Copy likely invoice PDFs into the archive before processing. Do not move or delete the originals.
|
|
50
|
+
4. Decide whether to check Gmail:
|
|
51
|
+
- Check Gmail when the user says to search Gmail, check mail, or download invoice emails.
|
|
52
|
+
- Skip Gmail when the user asks to organize local invoices or process files already in the directory.
|
|
53
|
+
- If ambiguous, infer from wording.
|
|
54
|
+
5. Process new PDFs in the target month folder, including PDFs downloaded from Gmail and files the user already placed there.
|
|
55
|
+
6. Extract fields, deduplicate, rename files, and write one CSV row per real expense.
|
|
56
|
+
7. Final-check new PDF count, new CSV rows, skipped duplicates, and any Gmail messages that could not be processed.
|
|
57
|
+
8. Commit only when the user asks for a commit or push.
|
|
58
|
+
|
|
59
|
+
## Gmail
|
|
60
|
+
|
|
61
|
+
Use connected Gmail tools when available. Do not use generic web search for Gmail mail.
|
|
62
|
+
|
|
63
|
+
Before Gmail work, check whether the Gmail connector/plugin is available. If unavailable, unauthorized, or inaccessible, notify the user clearly and continue any local-only workflow.
|
|
64
|
+
|
|
65
|
+
Search recent invoice emails with:
|
|
66
|
+
|
|
67
|
+
```text
|
|
68
|
+
newer_than:1d 发票 -in:spam -in:trash
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
For each Gmail hit, record sender, subject, received time, body summary, attachments, filenames, sizes, and MIME types. Process only messages with attachments or body links to PDF downloads.
|
|
72
|
+
|
|
73
|
+
Download usable invoice PDFs into the target month folder first, using original or temporary filenames. Prefer PDF for the final archive. Use XML as structured metadata. Treat OFD as auxiliary unless the user explicitly asks to archive it.
|
|
74
|
+
|
|
75
|
+
Rules:
|
|
76
|
+
|
|
77
|
+
- If a PDF attachment is `application/pdf`, read and download it directly when the connector allows it.
|
|
78
|
+
- If a PDF attachment is mislabeled as `application/octet-stream` and the connector cannot read or download it, mark it for manual Gmail web download; do not invent a file.
|
|
79
|
+
- If the body contains `PDF格式下载` and `XML格式下载`, use XML to identify fields and save the PDF as the final archived file.
|
|
80
|
+
- Deduplicate across emails by invoice number plus amount/date/seller before saving duplicates.
|
|
81
|
+
|
|
82
|
+
## Downloads Discovery
|
|
83
|
+
|
|
84
|
+
For macOS Downloads discovery, use filename keywords, MIME type, and PDF detection heuristics. Likely keywords include:
|
|
85
|
+
|
|
86
|
+
```text
|
|
87
|
+
发票 invoice receipt fapiao pdf 电子发票
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Copy likely PDFs into the archive root or target month folder before processing. Keep originals in Downloads untouched.
|
|
91
|
+
|
|
92
|
+
## Field Extraction
|
|
93
|
+
|
|
94
|
+
Prioritize these fields:
|
|
95
|
+
|
|
96
|
+
- Date: use Date paid or payment date first; otherwise use invoice issue date or 开票日期.
|
|
97
|
+
- Amount: use 价税合计, Total, or Amount paid.
|
|
98
|
+
- Purpose: summarize from 项目名称 or Description.
|
|
99
|
+
- Payment account: include receipt payment method when present, for example `Mastercard - 4516`.
|
|
100
|
+
- Remark: include seller/payee, invoice number, receipt number, order number, project detail, USD amount, and exchange rate as applicable.
|
|
101
|
+
|
|
102
|
+
## Naming Rules
|
|
103
|
+
|
|
104
|
+
Chinese invoice PDF:
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
YYYY-MM-DD+事项+金额.pdf
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Examples:
|
|
111
|
+
|
|
112
|
+
```text
|
|
113
|
+
2026-05-09+加油+362.00.pdf
|
|
114
|
+
2026-05-09+代订住宿服务费+387.00.pdf
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
USD service with both invoice and receipt:
|
|
118
|
+
|
|
119
|
+
```text
|
|
120
|
+
YYYY-MM-DD+服务名+人民币金额+invoice.pdf
|
|
121
|
+
YYYY-MM-DD+服务名+人民币金额+receipt.pdf
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Examples:
|
|
125
|
+
|
|
126
|
+
```text
|
|
127
|
+
2026-05-16+ChatGPT Plus订阅+138.00+invoice.pdf
|
|
128
|
+
2026-05-16+ChatGPT Plus订阅+138.00+receipt.pdf
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Classification
|
|
132
|
+
|
|
133
|
+
Use these defaults unless the extracted document indicates a better category:
|
|
134
|
+
|
|
135
|
+
- 成品油、汽油、加油站: 用途 `加油`, 分类 `交通费`
|
|
136
|
+
- 住宿、酒店、民宿: 用途 `代订住宿服务费` or `住宿`
|
|
137
|
+
- 沃尔玛、山姆、食品、日用品: 用途 `购物`, 分类 `办公/日用品` when appropriate
|
|
138
|
+
- 软件订阅、云服务、AI 服务: concrete service name, 分类 `软件服务费`
|
|
139
|
+
|
|
140
|
+
## CSV Rules
|
|
141
|
+
|
|
142
|
+
Write one row per real expense, not one row per file. If the same expense has both invoice and receipt, write one row and put both relative paths in `发票`, separated by Chinese semicolon `;`.
|
|
143
|
+
|
|
144
|
+
Use paths relative to the archive root:
|
|
145
|
+
|
|
146
|
+
```text
|
|
147
|
+
2026-05/2026-05-16+ChatGPT Plus订阅+138.00+invoice.pdf;2026-05/2026-05-16+ChatGPT Plus订阅+138.00+receipt.pdf
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Chinese invoice example:
|
|
151
|
+
|
|
152
|
+
```csv
|
|
153
|
+
2026-05-09,成品油电子发票(普通发票),362.00,,加油,交通费,,2026-05/2026-05-09+加油+362.00.pdf,发票号码:xxx;销售方:xxx;项目:92号汽油
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Invoice plus receipt example:
|
|
157
|
+
|
|
158
|
+
```csv
|
|
159
|
+
2026-05-16,Invoice/Receipt,138.00,Mastercard - 4516,ChatGPT Plus订阅,软件服务费,,2026-05/2026-05-16+ChatGPT Plus订阅+138.00+invoice.pdf;2026-05/2026-05-16+ChatGPT Plus订阅+138.00+receipt.pdf,OpenAI ChatGPT Plus Subscription (per seat);金额:20.00 USD;汇率:6.9;Invoice:xxx;Receipt:xxx
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Receipt-only example:
|
|
163
|
+
|
|
164
|
+
```csv
|
|
165
|
+
2026-05-16,Receipt,138.00,Mastercard - 4516,ChatGPT Plus订阅,软件服务费,,2026-05/2026-05-16+ChatGPT Plus订阅+138.00+receipt.pdf,仅有收据,无 invoice;OpenAI ChatGPT Plus Subscription (per seat);金额:20.00 USD;汇率:6.9;Receipt:2079-5046-5382
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## USD Services
|
|
169
|
+
|
|
170
|
+
Always write RMB in the `金额` column, not USD.
|
|
171
|
+
|
|
172
|
+
Use fixed exchange rate `6.9` unless the user says otherwise:
|
|
173
|
+
|
|
174
|
+
```text
|
|
175
|
+
RMB = USD * 6.9
|
|
176
|
+
20.00 USD * 6.9 = 138.00 RMB
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
For USD services:
|
|
180
|
+
|
|
181
|
+
- 金额: RMB with two decimals
|
|
182
|
+
- 用途: concrete service, for example `ChatGPT Plus订阅`
|
|
183
|
+
- 分类: usually `软件服务费`
|
|
184
|
+
- 支付账户: receipt card/account if present
|
|
185
|
+
- 备注: service content, USD amount, exchange rate, and invoice/receipt numbers
|
|
186
|
+
|
|
187
|
+
## Deduplication
|
|
188
|
+
|
|
189
|
+
Treat files as duplicates when invoice/receipt number, amount, date, and seller/payee match.
|
|
190
|
+
|
|
191
|
+
For duplicates:
|
|
192
|
+
|
|
193
|
+
- Keep one archived file.
|
|
194
|
+
- Write only one CSV row.
|
|
195
|
+
- Do not count duplicate downloads as additional expenses.
|
|
196
|
+
|
|
197
|
+
## Git
|
|
198
|
+
|
|
199
|
+
Only commit or push when the user asks.
|
|
200
|
+
|
|
201
|
+
Before committing:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
git status --short
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Confirm changes are only invoice archive changes intended for the run. Do not commit `.DS_Store`.
|
|
208
|
+
|
|
209
|
+
Suggested commit message for archive updates:
|
|
210
|
+
|
|
211
|
+
```text
|
|
212
|
+
Update invoice archive
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Suggested commit message for workflow/rule changes:
|
|
216
|
+
|
|
217
|
+
```text
|
|
218
|
+
Update invoice workflow
|
|
219
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import csv
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
HEADER = ["日期", "类型", "金额", "支付账户", "用途", "分类", "是否报销", "发票", "备注"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def ensure_month(year: int, month: int, root: Path) -> tuple[Path, Path]:
|
|
11
|
+
month_dir = root / f"{year:04d}-{month:02d}"
|
|
12
|
+
csv_path = root / f"{year:04d}_{month}.csv"
|
|
13
|
+
|
|
14
|
+
month_dir.mkdir(parents=True, exist_ok=True)
|
|
15
|
+
|
|
16
|
+
if not csv_path.exists():
|
|
17
|
+
with csv_path.open("w", newline="", encoding="utf-8-sig") as handle:
|
|
18
|
+
writer = csv.writer(handle)
|
|
19
|
+
writer.writerow(HEADER)
|
|
20
|
+
|
|
21
|
+
return month_dir, csv_path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main() -> int:
|
|
25
|
+
parser = argparse.ArgumentParser(description="Create an invoice month folder and CSV ledger.")
|
|
26
|
+
parser.add_argument("year", type=int)
|
|
27
|
+
parser.add_argument("month", type=int)
|
|
28
|
+
parser.add_argument("--root", default=".", help="Archive root. Defaults to current directory.")
|
|
29
|
+
args = parser.parse_args()
|
|
30
|
+
|
|
31
|
+
if args.month < 1 or args.month > 12:
|
|
32
|
+
parser.error("month must be between 1 and 12")
|
|
33
|
+
|
|
34
|
+
month_dir, csv_path = ensure_month(args.year, args.month, Path(args.root).resolve())
|
|
35
|
+
print(f"Ensured folder: {month_dir}")
|
|
36
|
+
print(f"Ensured CSV: {csv_path}")
|
|
37
|
+
return 0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
if __name__ == "__main__":
|
|
41
|
+
raise SystemExit(main())
|