Back to skills
SkillHub ClubShip Full StackFull Stack
openclaw-receipt-manager
Imported from https://github.com/openclaw/skills.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Stars
3,124
Hot score
99
Updated
March 20, 2026
Overall rating
C4.0
Composite score
4.0
Best-practice grade
F17.6
Install command
npx @skill-hub/cli install openclaw-skills-openclaw-receipt-manager
Repository
openclaw/skills
Skill path: skills/clinchcc/openclaw-receipt-manager
Imported from https://github.com/openclaw/skills.
Open repositoryBest for
Primary workflow: Ship Full Stack.
Technical facets: Full Stack.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: openclaw.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install openclaw-receipt-manager into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/openclaw/skills before adding openclaw-receipt-manager to shared team environments
- Use openclaw-receipt-manager for development workflows
Works across
Claude CodeCodex CLIGemini CLIOpenCode
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: receipt-manager
description: Receipt management skill. Use when: (1) User sends a receipt image, (2) User asks about expenses or receipts, (3) User wants monthly spending summary.
---
# Receipt Manager
Store and query receipt data locally.
## Trigger
- receipt, expense, invoice, spending, claim
## How to Use
### 1. Initialize (first time)
```bash
python3 scripts/receipt_db.py init
```
### 2. Add Receipt
After OpenClaw recognizes the receipt image, the data is saved automatically via handler.
### 3. Query
```bash
# List all
python3 scripts/receipt_db.py list
# Search
python3 scripts/receipt_db.py search --q "walmart"
# Monthly summary
python3 scripts/receipt_db.py summary --month 2026-02
```
## Files
- `scripts/receipt_db.py` - Main CLI tool
- `scripts/handler.py` - Receives JSON from OpenClaw, saves to DB
- `data/receipts/` - Local SQLite DB and images
## Privacy
All data stored locally on your machine. No cloud upload.
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### scripts/receipt_db.py
```python
#!/usr/bin/env python3
"""
Receipt manager DB + ingestion CLI.
Usage examples:
python3 scripts/receipt_db.py init
python3 scripts/receipt_db.py add --image ~/Downloads/r.jpg --text "Green Fresh Supermarket Total $2.47 2026-02-26"
python3 scripts/receipt_db.py add --image ~/Downloads/r.jpg
python3 scripts/receipt_db.py search --q grocery
python3 scripts/receipt_db.py show --id 1
python3 scripts/receipt_db.py summary --month 2026-02
"""
from __future__ import annotations
import argparse
import datetime as dt
import hashlib
import json
import os
import re
import shutil
import sqlite3
import subprocess
import sys
from pathlib import Path
from typing import Optional
ROOT = Path(__file__).resolve().parent.parent
DATA_DIR = ROOT / "data" / "receipts"
DB_PATH = DATA_DIR / "db.sqlite3"
IMG_DIR = DATA_DIR / "images"
CATEGORIES = {
"grocery": ["supermarket", "grocery", "freshco", "whole foods", "costco", "market", "mart", "trader joe", "save on"],
"dining": ["restaurant", "cafe", "coffee", "tea", "diner", "pizza", "burger", "sushi", "bbq", "kitchen"],
"transport": ["uber", "lyft", "taxi", "gas", "fuel", "petro", "shell", "chevron", "parking", "transit"],
"health": ["pharmacy", "drug", "clinic", "hospital", "dental", "health", "medicine"],
"shopping": ["amazon", "walmart", "target", "store", "shop", "mall", "ikea", "best buy"],
"travel": ["hotel", "airbnb", "airlines", "flight", "booking", "expedia"],
"utilities": ["hydro", "electric", "internet", "phone", "water", "utility", "telus", "rogers", "bell"],
}
DATE_PATTERNS = [
re.compile(r"\b(20\d{2})[-/](\d{1,2})[-/](\d{1,2})\b"), # YYYY-MM-DD
re.compile(r"\b(\d{1,2})[-/](\d{1,2})[-/](20\d{2})\b"), # MM/DD/YYYY
]
AMOUNT_PATTERNS = [
re.compile(r"(?i)(?:total|amount|sum)\s*[::]?\s*([\$€£¥]?\s?\d+[\.,]\d{2})"),
re.compile(r"([\$€£¥]\s?\d+[\.,]\d{2})"),
]
def ensure_dirs() -> None:
IMG_DIR.mkdir(parents=True, exist_ok=True)
def connect() -> sqlite3.Connection:
ensure_dirs()
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def init_db(verbose: bool = False) -> None:
with connect() as conn:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS receipts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TEXT NOT NULL,
vendor TEXT,
receipt_date TEXT,
currency TEXT,
total REAL,
category TEXT,
image_path TEXT NOT NULL,
image_sha256 TEXT NOT NULL UNIQUE,
ocr_text TEXT,
items_json TEXT,
meta_json TEXT
)
"""
)
conn.execute("CREATE INDEX IF NOT EXISTS idx_receipts_date ON receipts(receipt_date)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_receipts_vendor ON receipts(vendor)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_receipts_category ON receipts(category)")
cols = {r[1] for r in conn.execute("PRAGMA table_info(receipts)").fetchall()}
if "items_json" not in cols:
conn.execute("ALTER TABLE receipts ADD COLUMN items_json TEXT")
if verbose:
print(f"Initialized: {DB_PATH}")
def sha256_file(path: Path) -> str:
h = hashlib.sha256()
with path.open("rb") as f:
for chunk in iter(lambda: f.read(1024 * 1024), b""):
h.update(chunk)
return h.hexdigest()
def copy_image_dedup(src: Path) -> tuple[str, str]:
ext = src.suffix.lower() or ".jpg"
digest = sha256_file(src)
dst = IMG_DIR / f"{digest}{ext}"
if not dst.exists():
shutil.copy2(src, dst)
rel = str(dst.relative_to(ROOT))
return rel, digest
def try_ocr_tesseract(image: Path) -> Optional[str]:
tesseract = shutil.which("tesseract")
if not tesseract:
return None
try:
proc = subprocess.run(
[tesseract, str(image), "stdout", "-l", "eng+chi_sim"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=False,
timeout=30,
)
if proc.returncode == 0:
txt = (proc.stdout or "").strip()
return txt or None
except Exception:
pass
return None
def normalize_date(text: str) -> Optional[str]:
for idx, pat in enumerate(DATE_PATTERNS):
m = pat.search(text)
if not m:
continue
try:
if idx == 0:
y, mo, d = map(int, m.groups())
else:
mo, d, y = map(int, m.groups())
return dt.date(y, mo, d).isoformat()
except Exception:
continue
return None
def parse_total_and_currency(text: str) -> tuple[Optional[float], Optional[str]]:
for pat in AMOUNT_PATTERNS:
m = pat.search(text)
if not m:
continue
raw = m.group(1).replace(" ", "")
currency = None
if raw and raw[0] in "$€£¥":
currency = {"$": "USD", "€": "EUR", "£": "GBP", "¥": "CNY"}.get(raw[0], None)
raw = raw[1:]
raw = raw.replace(",", ".")
try:
return float(raw), currency
except Exception:
pass
return None, None
def parse_vendor(text: str) -> Optional[str]:
lines = [ln.strip() for ln in text.splitlines() if ln.strip()]
if not lines:
return None
for ln in lines[:6]:
if re.search(r"(?i)receipt|invoice|tax|total|date|thank", ln):
continue
if 2 <= len(ln) <= 48:
return ln
return lines[0][:48]
def auto_category(vendor: Optional[str], text: str) -> str:
blob = f"{vendor or ''} {text}".lower()
for cat, kws in CATEGORIES.items():
if any(kw in blob for kw in kws):
return cat
return "other"
def parse_items(text: str) -> list[dict[str, object]]:
items: list[dict[str, object]] = []
for raw in text.splitlines():
line = raw.strip()
if not line or len(line) < 3:
continue
m = re.search(r"(.+?)\s+([\$€£¥]?\d+[\.,]\d{2})$", line)
if not m:
continue
name = m.group(1).strip(" -:\t")
price_raw = m.group(2).replace(" ", "")
currency = None
if price_raw[0] in "$€£¥":
currency = {"$": "USD", "€": "EUR", "£": "GBP", "¥": "CNY"}.get(price_raw[0])
price_raw = price_raw[1:]
try:
price = float(price_raw.replace(",", "."))
except Exception:
continue
if name and not re.search(r"(?i)total|subtotal|tax|amount|sum", name):
items.append({"name": name[:120], "price": price, "currency": currency})
return items
def add_receipt(args: argparse.Namespace) -> int:
image = Path(args.image).expanduser().resolve()
# Security: prevent path traversal
if '..' in str(image):
print("ERROR: invalid image path (path traversal)")
return 2
# Validate image is within allowed directory (home only for security)
if not image.is_relative_to(Path.home()):
print(f"ERROR: image must be in home directory: {image}")
return 2
if not image.exists():
print(f"ERROR: image not found: {image}")
return 2
init_db()
rel_image, digest = copy_image_dedup(image)
ocr_text = args.text.strip() if args.text else None
ocr_engine = None
if not ocr_text:
ocr_text = try_ocr_tesseract(image)
if ocr_text:
ocr_engine = "tesseract"
text_for_parse = ocr_text or ""
vendor = args.vendor or parse_vendor(text_for_parse)
receipt_date = args.date or normalize_date(text_for_parse)
total, currency = (None, None)
if args.total is not None:
total = float(args.total)
else:
total, currency = parse_total_and_currency(text_for_parse)
if args.currency:
currency = args.currency.upper()
category = args.category or auto_category(vendor, text_for_parse)
parsed_items = []
if args.items_json:
try:
parsed_items = json.loads(args.items_json)
except Exception:
parsed_items = []
elif text_for_parse:
parsed_items = parse_items(text_for_parse)
meta = {
"ocr_engine": ocr_engine,
"ocr_available": bool(ocr_text),
"source_image": str(image),
}
with connect() as conn:
row = conn.execute("SELECT id FROM receipts WHERE image_sha256 = ?", (digest,)).fetchone()
if row:
print(json.dumps({"status": "duplicate", "receipt_id": row["id"], "image": rel_image}, ensure_ascii=False, indent=2))
return 0
cur = conn.execute(
"""
INSERT INTO receipts(
created_at, vendor, receipt_date, currency, total, category,
image_path, image_sha256, ocr_text, items_json, meta_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
dt.datetime.now(dt.UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z"),
vendor,
receipt_date,
currency,
total,
category,
rel_image,
digest,
ocr_text,
json.dumps(parsed_items, ensure_ascii=False),
json.dumps(meta, ensure_ascii=False),
),
)
rid = cur.lastrowid
out = {
"status": "ok",
"receipt_id": rid,
"vendor": vendor,
"date": receipt_date,
"total": total,
"currency": currency,
"category": category,
"image": rel_image,
"db": str(DB_PATH.relative_to(ROOT)),
"ocr": "provided" if args.text else ("tesseract" if ocr_engine else "none"),
"item_count": len(parsed_items),
}
print(json.dumps(out, ensure_ascii=False, indent=2))
return 0
def search_receipts(args: argparse.Namespace) -> int:
init_db()
q = (args.q or "").strip()
item = (args.item or "").strip()
clauses = []
params: list[object] = []
if q:
like = f"%{q}%"
clauses.append("(vendor LIKE ? OR category LIKE ? OR ocr_text LIKE ? OR items_json LIKE ?)")
params.extend([like, like, like, like])
if item:
clauses.append("items_json LIKE ?")
params.append(f"%{item}%")
wh = f"WHERE {' AND '.join(clauses)}" if clauses else ""
sql = f"SELECT id, receipt_date, vendor, total, currency, category, image_path, items_json FROM receipts {wh} ORDER BY id DESC LIMIT ?"
params.append(args.limit)
with connect() as conn:
rows = [dict(r) for r in conn.execute(sql, params).fetchall()]
print(json.dumps(rows, ensure_ascii=False, indent=2))
return 0
def show_receipt(args: argparse.Namespace) -> int:
init_db()
with connect() as conn:
row = conn.execute("SELECT * FROM receipts WHERE id = ?", (args.id,)).fetchone()
if not row:
print(f"Not found: {args.id}")
return 1
d = dict(row)
if d.get("meta_json"):
try:
d["meta_json"] = json.loads(d["meta_json"])
except Exception:
pass
if d.get("items_json"):
try:
d["items_json"] = json.loads(d["items_json"])
except Exception:
pass
print(json.dumps(d, ensure_ascii=False, indent=2))
return 0
def summary(args: argparse.Namespace) -> int:
init_db()
month = args.month or dt.date.today().strftime("%Y-%m")
if not re.match(r"^20\d{2}-\d{2}$", month):
print("month must be YYYY-MM")
return 2
like = f"{month}-%"
with connect() as conn:
by_cat = [
dict(r)
for r in conn.execute(
"""
SELECT category, COUNT(*) AS count, ROUND(COALESCE(SUM(total),0),2) AS total
FROM receipts
WHERE receipt_date LIKE ?
GROUP BY category
ORDER BY total DESC
""",
(like,),
).fetchall()
]
by_vendor = [
dict(r)
for r in conn.execute(
"""
SELECT COALESCE(vendor, 'Unknown') AS vendor, COUNT(*) AS count, ROUND(COALESCE(SUM(total),0),2) AS total
FROM receipts
WHERE receipt_date LIKE ?
GROUP BY COALESCE(vendor, 'Unknown')
ORDER BY total DESC
LIMIT ?
""",
(like, args.vendor_limit),
).fetchall()
]
print(json.dumps({"month": month, "by_category": by_cat, "by_vendor": by_vendor}, ensure_ascii=False, indent=2))
return 0
def list_receipts(args: argparse.Namespace) -> int:
init_db()
clauses = []
params: list[object] = []
if args.month:
if re.match(r"^\d{1,2}$", args.month):
y = dt.date.today().year
m = int(args.month)
month = f"{y}-{m:02d}"
else:
month = args.month
clauses.append("receipt_date LIKE ?")
params.append(f"{month}-%")
if args.category:
clauses.append("LOWER(category)=LOWER(?)")
params.append(args.category)
where = ("WHERE " + " AND ".join(clauses)) if clauses else ""
sql = f"SELECT id, receipt_date, vendor, total, currency, category, items_json FROM receipts {where} ORDER BY receipt_date DESC, id DESC LIMIT ?"
params.append(args.limit)
with connect() as conn:
rows = [dict(r) for r in conn.execute(sql, params).fetchall()]
print(json.dumps(rows, ensure_ascii=False, indent=2))
return 0
def nlp_query(args: argparse.Namespace) -> int:
init_db()
t = (args.text or "").strip()
if not t:
print(json.dumps({"status": "error", "message": "empty text"}, ensure_ascii=False, indent=2))
return 2
m = re.search(r"查\s*(.+?)\s*在.*收据", t)
if m:
item = m.group(1).strip()
ns = argparse.Namespace(q="", item=item, limit=args.limit)
return search_receipts(ns)
m2 = re.search(r"列出\s*(\d{1,2})\s*月\s*(\S+?)类?收据", t)
if m2:
month = m2.group(1)
cat = m2.group(2)
ns = argparse.Namespace(month=month, category=cat, limit=args.limit)
return list_receipts(ns)
if "汇总" in t and re.search(r"\d{4}-\d{2}", t):
month = re.search(r"(\d{4}-\d{2})", t).group(1)
ns = argparse.Namespace(month=month, vendor_limit=10)
return summary(ns)
print(json.dumps({
"status": "unrecognized",
"hint": [
"查吹风机在哪个收据",
"列出2月购物类收据",
"汇总 2026-02"
]
}, ensure_ascii=False, indent=2))
return 0
def update_receipt(args: argparse.Namespace) -> int:
init_db()
updates: dict[str, object] = {}
if args.vendor is not None:
updates["vendor"] = args.vendor
if args.date is not None:
updates["receipt_date"] = args.date
if args.total is not None:
updates["total"] = float(args.total)
if args.currency is not None:
updates["currency"] = args.currency.upper() if args.currency else None
if args.category is not None:
updates["category"] = args.category
if args.text is not None:
updates["ocr_text"] = args.text
if args.items_json is not None:
try:
json.loads(args.items_json)
updates["items_json"] = args.items_json
except Exception:
print("--items-json must be valid JSON array")
return 2
if not updates:
print("No fields to update. Provide at least one of --vendor/--date/--total/--currency/--category/--text/--items-json")
return 2
cols = ", ".join([f"{k} = ?" for k in updates.keys()])
params = list(updates.values()) + [args.id]
with connect() as conn:
row = conn.execute("SELECT id FROM receipts WHERE id = ?", (args.id,)).fetchone()
if not row:
print(f"Not found: {args.id}")
return 1
conn.execute(f"UPDATE receipts SET {cols} WHERE id = ?", params)
print(json.dumps({"status": "ok", "updated_id": args.id, "fields": list(updates.keys())}, ensure_ascii=False, indent=2))
return 0
def delete_receipt(args: argparse.Namespace) -> int:
init_db()
with connect() as conn:
row = conn.execute("SELECT id, image_path FROM receipts WHERE id = ?", (args.id,)).fetchone()
if not row:
print(f"Not found: {args.id}")
return 1
if not args.yes:
print(json.dumps({
"status": "confirm_required",
"id": args.id,
"hint": "re-run with --yes to confirm deletion"
}, ensure_ascii=False, indent=2))
return 2
conn.execute("DELETE FROM receipts WHERE id = ?", (args.id,))
if args.delete_image and row["image_path"]:
img = ROOT / row["image_path"]
try:
if img.exists():
img.unlink()
except Exception:
pass
print(json.dumps({
"status": "ok",
"deleted_id": args.id,
"image_deleted": bool(args.delete_image)
}, ensure_ascii=False, indent=2))
return 0
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(description="Receipt manager CLI")
sp = p.add_subparsers(dest="cmd", required=True)
sp.add_parser("init", help="Initialize DB")
p_add = sp.add_parser("add", help="Add one receipt")
p_add.add_argument("--image", required=True, help="Path to receipt image")
p_add.add_argument("--text", help="Optional OCR text (if omitted, tries local OCR)")
p_add.add_argument("--vendor")
p_add.add_argument("--date", help="YYYY-MM-DD")
p_add.add_argument("--total", type=float)
p_add.add_argument("--currency", help="USD/CAD/CNY...")
p_add.add_argument("--category", help="grocery/dining/.../other")
p_add.add_argument("--items-json", help="JSON array of items, e.g. [{\"name\":\"dryer\",\"price\":99.0}]")
p_search = sp.add_parser("search", help="Search receipts")
p_search.add_argument("--q", default="")
p_search.add_argument("--item", default="", help="Search in items_json by item keyword")
p_search.add_argument("--limit", type=int, default=20)
p_show = sp.add_parser("show", help="Show receipt by id")
p_show.add_argument("--id", type=int, required=True)
p_sum = sp.add_parser("summary", help="Monthly summary")
p_sum.add_argument("--month", help="YYYY-MM")
p_sum.add_argument("--vendor-limit", type=int, default=10)
p_list = sp.add_parser("list", help="List receipts with filters")
p_list.add_argument("--month", help="YYYY-MM or M")
p_list.add_argument("--category", help="category filter")
p_list.add_argument("--limit", type=int, default=20)
p_nlp = sp.add_parser("nlp", help="Natural language query helper")
p_nlp.add_argument("--text", required=True, help="e.g. 查吹风机在哪个收据")
p_nlp.add_argument("--limit", type=int, default=20)
p_upd = sp.add_parser("update", help="Update receipt fields")
p_upd.add_argument("--id", type=int, required=True)
p_upd.add_argument("--vendor")
p_upd.add_argument("--date", help="YYYY-MM-DD")
p_upd.add_argument("--total", type=float)
p_upd.add_argument("--currency")
p_upd.add_argument("--category")
p_upd.add_argument("--text", help="Replace OCR text")
p_upd.add_argument("--items-json", help="Replace items_json with a JSON array")
p_del = sp.add_parser("delete", help="Delete receipt by id")
p_del.add_argument("--id", type=int, required=True)
p_del.add_argument("--yes", action="store_true", help="Confirm deletion")
p_del.add_argument("--delete-image", action="store_true", help="Also delete stored image file")
return p
def main() -> int:
args = build_parser().parse_args()
if args.cmd == "init":
init_db(verbose=True)
return 0
if args.cmd == "add":
return add_receipt(args)
if args.cmd == "search":
return search_receipts(args)
if args.cmd == "show":
return show_receipt(args)
if args.cmd == "summary":
return summary(args)
if args.cmd == "list":
return list_receipts(args)
if args.cmd == "nlp":
return nlp_query(args)
if args.cmd == "update":
return update_receipt(args)
if args.cmd == "delete":
return delete_receipt(args)
return 1
if __name__ == "__main__":
raise SystemExit(main())
```
### scripts/handler.py
```python
#!/usr/bin/env python3
"""
Receipt skill handler for OpenClaw.
Receives extracted receipt info and saves to database.
"""
import sys
import json
import subprocess
import re
from pathlib import Path
import shlex
SCRIPT_DIR = Path(__file__).parent
def sanitize_string(s, max_len=200):
"""Sanitize string input to prevent injection."""
if not s:
return ''
# Remove any shell metacharacters
s = re.sub(r'[;&|`$<>]', '', str(s))
return s[:max_len]
def validate_path(p):
"""Validate and sanitize file path."""
if not p:
return ''
# Only allow alphanumeric, dash, underscore, dot, slash
if not re.match(r'^[\w\-./]+$', p):
return ''
# Prevent path traversal
if '..' in p:
return ''
return p
def handle(data):
"""Handle receipt data from OpenClaw."""
required = ['vendor', 'total']
for field in required:
if field not in data:
return {"ok": False, "error": f"missing {field}"}
# Validate and sanitize inputs
vendor = sanitize_string(data.get('vendor', ''), 100)
if not vendor:
return {"ok": False, "error": "invalid vendor"}
try:
total = float(data.get('total', 0))
if total < 0 or total > 1000000:
return {"ok": False, "error": "invalid total amount"}
except (ValueError, TypeError):
return {"ok": False, "error": "total must be a number"}
date = sanitize_string(data.get('date', ''), 20)
currency = sanitize_string(data.get('currency', 'CAD'), 10)
category = sanitize_string(data.get('category', 'other'), 50)
# Validate image path if provided
image_path = validate_path(data.get('image', ''))
text = sanitize_string(data.get('text', ''), 1000)
# Build command with sanitized args
cmd = [
str(SCRIPT_DIR / "receipt_db.py"),
"add",
"--vendor", vendor,
"--total", str(total),
]
if date:
cmd.extend(["--date", date])
if currency:
cmd.extend(["--currency", currency])
if category:
cmd.extend(["--category", category])
if image_path:
cmd.extend(["--image", image_path])
if text:
cmd.extend(["--text", text])
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
if result.returncode == 0:
return {"ok": True, "message": f"✅ 已保存收据: {vendor} ${total} {currency}"}
else:
return {"ok": False, "error": result.stderr[:200]}
except subprocess.TimeoutExpired:
return {"ok": False, "error": "timeout"}
except Exception as e:
return {"ok": False, "error": str(e)[:100]}
if __name__ == '__main__':
try:
data = json.load(sys.stdin)
result = handle(data)
print(json.dumps(result))
except json.JSONDecodeError:
print(json.dumps({"ok": False, "error": "invalid json input"}))
except Exception as e:
print(json.dumps({"ok": False, "error": str(e)[:100]}))
```
---
## Skill Companion Files
> Additional files collected from the skill directory layout.
### README.md
```markdown
# 🎫 Receipt Manager Skill
**English** | [中文](#收据管理器技能)
---
## <a name="english"></a>English
### Quick Start
1. **Install**: `git clone https://github.com/clinchcc/openclaw-receipt-manager.git ~/.openclaw/workspace/skills/receipt`
2. **Init**: `python3 ~/.openclaw/workspace/skills/receipt/scripts/receipt_db.py init`
3. **Use**: Send receipt image to OpenClaw
### Commands
```bash
# List receipts
python3 scripts/receipt_db.py list
# Search
python3 scripts/receipt_db.py search --q "walmart"
# Monthly summary
python3 scripts/receipt_db.py summary --month 2026-02
```
### Files
- `scripts/receipt_db.py` - Main CLI
- `scripts/handler.py` - OpenClaw handler
- `data/receipts/` - Local SQLite DB + images
### Privacy
✅ All data stored **locally** - nothing sent to cloud
---
## <a name="收据管理器技能"></a>收据管理器技能
**[English](#english)** | 中文
### 快速开始
1. **安装**: `git clone https://github.com/clinchcc/openclaw-receipt-manager.git ~/.openclaw/workspace/skills/receipt`
2. **初始化**: `python3 ~/.openclaw/workspace/skills/receipt/scripts/receipt_db.py init`
3. **使用**: 发送收据图片给 OpenClaw
### 命令
```bash
# 列出收据
python3 scripts/receipt_db.py list
# 搜索
python3 scripts/receipt_db.py search --q "沃尔玛"
# 月度汇总
python3 scripts/receipt_db.py summary --month 2026-02
```
### 文件
- `scripts/receipt_db.py` - 主 CLI
- `scripts/handler.py` - OpenClaw 处理器
- `data/receipts/` - 本地 SQLite 数据库 + 图片
### 隐私
✅ 所有数据**本地存储** - 不上传云端
```
### _meta.json
```json
{
"owner": "clinchcc",
"slug": "openclaw-receipt-manager",
"displayName": "Openclaw Receipt Manager",
"latest": {
"version": "0.1.7",
"publishedAt": 1772329973465,
"commit": "https://github.com/openclaw/skills/commit/28faa36d70e8a7cffb4718c6d1e166d4b977dbc7"
},
"history": []
}
```