@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 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,7 @@
1
+ interface:
2
+ display_name: "Fapiao"
3
+ short_description: "Organize invoice PDFs and CSV ledgers"
4
+ default_prompt: "Use $fapiao to organize this month's invoice PDFs and update the CSV ledger."
5
+
6
+ policy:
7
+ allow_implicit_invocation: true
@@ -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())