life-capture
capture daily-life notes into markdown and sqlite. use when the user wants to record one or more life entries such as expenses, completed tasks, schedules, reminders, or ideas; classify the content; generate tags; parse natural language into structured json; write a daily markdown note under life/daily; and sync structured fields into a local sqlite database. triggers include short single-line entries, mixed sentences containing multiple record types, or requests to log and organize personal information for later review and reporting.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install openclaw-skills-life-capture
Repository
Skill path: skills/epitomizelu/life-capture
capture daily-life notes into markdown and sqlite. use when the user wants to record one or more life entries such as expenses, completed tasks, schedules, reminders, or ideas; classify the content; generate tags; parse natural language into structured json; write a daily markdown note under life/daily; and sync structured fields into a local sqlite database. triggers include short single-line entries, mixed sentences containing multiple record types, or requests to log and organize personal information for later review and reporting.
Open repositoryBest for
Primary workflow: Write Technical Docs.
Technical facets: Full Stack, Backend, Tech Writer.
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 life-capture into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/openclaw/skills before adding life-capture to shared team environments
- Use life-capture for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: life-capture
description: capture daily-life notes into markdown and sqlite. use when the user wants to record one or more life entries such as expenses, completed tasks, schedules, reminders, or ideas; classify the content; generate tags; parse natural language into structured json; write a daily markdown note under life/daily; and sync structured fields into a local sqlite database. triggers include short single-line entries, mixed sentences containing multiple record types, or requests to log and organize personal information for later review and reporting.
---
# life-capture
Turn natural-language life logs into durable records. This skill classifies each input item, generates tags, creates user-visible markdown, writes to a daily note under `life/daily`, and syncs structured data into `life/db/life.db`.
## Default storage layout
Use these paths unless the user explicitly overrides them:
```text
life/
daily/
ideas/
db/life.db
```
Create missing directories as needed. Never delete existing content. Append or update only.
## Supported record types
Map every parsed item to exactly one primary type:
- `expense`: spending, bills, purchases, subscriptions, refunds
- `task`: completed tasks, ongoing work, todos, chores, habits
- `schedule`: calendar items, appointments, time blocks, plans
- `idea`: ideas, inspiration, possible projects, reflections worth saving
When a sentence contains multiple items, split it into multiple records.
## Output contract
For each user request:
1. Parse the message into one or more records.
2. Generate a stable `id` for each record using the pattern:
- `exp_YYYYMMDD_NNN`
- `task_YYYYMMDD_NNN`
- `sched_YYYYMMDD_NNN`
- `idea_YYYYMMDD_NNN`
3. Generate 1 to 4 short tags.
4. Show the user the organized result in markdown.
5. Save the records by running `scripts/process_entry.py`.
Always keep the original user wording in `raw_text`. Never invent missing fields. Leave unknown fields null.
## User-visible response format
Because this skill is configured for visible output, show a concise but complete result after writing:
```md
## 已整理记录
### 1) <type label>
- ID: <id>
- 标签: #a #b
- 归档: <daily markdown path>
- 数据库: <written/skipped>
#### Markdown
<the markdown block written for this item>
#### JSON
```json
<the parsed record json>
```
```
If there are multiple records, repeat the block for each one.
## Parsing rules
Use `scripts/parse_entries.py` for natural-language parsing. The parser now reads configurable rules from `references/parser_config.json`, so prefer editing that file instead of changing Python when you need new categories, tags, or keyword mappings.
### Expense
Extract when present:
- `amount`
- `currency` (default `CNY` only when the currency symbol or language implies RMB; otherwise null)
- `category`
- `subcategory`
- `merchant`
- `pay_method`
Default top-level tags often include `开销` plus one semantic tag such as `餐饮` or `交通`.
Preferred categories:
- 饮食
- 交通
- 购物
- 居家
- 社交
- 娱乐
- 医疗
- 学习
- 其他
### Task
Extract when present:
- `status` (`todo`, `doing`, `done`, `cancelled`)
- `priority` (`low`, `normal`, `high`)
- `project`
- `due_date`
- `completed_at`
If the user says they already did something, default status to `done`.
### Schedule
Extract when present:
- `schedule_date`
- `start_time`
- `end_time`
- `location`
- `status` (`planned`, `done`, `skipped`)
If the user uses relative dates, resolve them from the current conversation date. Prefer passing `--today YYYY-MM-DD` to `scripts/process_entry.py` or `scripts/parse_entries.py` so relative dates like `明天` are stable across environments.
### Idea
Extract when present:
- `idea_type`
- `status` (`captured`, `reviewing`, `used`, `archived`)
- `related_task_id`
Default status to `captured`.
## Configurable parsing rules
Before editing Python, check whether the change can be made in `references/parser_config.json`.
You can change:
- category and subcategory mappings for expenses
- task project mappings
- idea type mappings
- schedule extra tag mappings
- default tags by record type
- hint regexes used in type inference
To test a modified config without changing the bundled default file:
```bash
python scripts/parse_entries.py --config /path/to/custom_config.json --text "买咖啡 18 元,明天下午两点去体检"
```
## Markdown writing rules
Write each record into the daily note for its effective date under one of these sections:
- `## 开销`
- `## 任务`
- `## 日程`
- `## 灵感`
Use this block structure:
```md
### <id>
- 时间:<time or empty>
- 标签:#tag1 #tag2
- 原始描述:<raw_text>
- 摘要:<summary>
```
Then add type-specific fields:
- Expense: 金额 / 币种 / 分类 / 子分类 / 商家 / 支付方式
- Task: 状态 / 优先级 / 项目 / 截止日期 / 完成时间
- Schedule: 日期 / 开始时间 / 结束时间 / 地点 / 状态
- Idea: 类型 / 状态 / 关联任务
## Execution workflow
### End-to-end one-command flow
Use this when the user provides natural language and wants the records saved immediately:
```bash
python scripts/process_entry.py --root life --db life/db/life.db --today 2026-03-10 --text "今天中午牛肉面 26 元,下午整理了书桌,想到可以做一个生活数据看板"
```
The wrapper script will:
1. initialize the database if missing
2. parse text into `{"records": [...]}` with `scripts/parse_entries.py`
3. save markdown and sqlite rows with `scripts/save_entry.py`
4. print parsed records plus save results as json
### Split-step flow
Use this when the user asks to inspect or verify the structured output before writing:
```bash
python scripts/parse_entries.py --text "明天下午两点去体检,买咖啡 18 元"
```
Then save:
```bash
python scripts/save_entry.py --root life --db life/db/life.db --stdin-json
```
### Database init only
Use this once before first write if `life/db/life.db` does not exist and you are not using `process_entry.py`:
```bash
python scripts/init_db.py --db life/db/life.db
```
## Database sync rules
The database design is:
- `entries`
- `expenses`
- `tasks`
- `schedules`
- `ideas`
- `tags`
- `entry_tags`
See `references/schema.md` for the schema, `references/examples.md` for sample payloads and commands, and `references/configuration.md` plus `references/parser_config.json` for configurable parsing rules.
## Failure handling
- If markdown write succeeds but database sync fails, say so clearly.
- Do not silently drop a record.
- If parsing is ambiguous, make the narrowest safe interpretation and preserve the original text.
- If a record is missing a critical type-specific field, still save the record with null fields rather than discarding it.
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### scripts/parse_entries.py
```python
#!/usr/bin/env python3
import argparse
import json
import re
import sqlite3
from collections import defaultdict
from datetime import date, datetime, timedelta
from pathlib import Path
from typing import Dict, List, Optional, Tuple
TYPE_PREFIX = {
"expense": "exp",
"task": "task",
"schedule": "sched",
"idea": "idea",
}
WEEKDAY_MAP = {
"一": 0, "二": 1, "三": 2, "四": 3, "五": 4, "六": 5, "日": 6, "天": 6,
}
DEFAULT_CONFIG = {
"expense_category_rules": [],
"expense_subcategory_rules": [],
"idea_type_rules": [],
"task_project_rules": [],
"schedule_tag_rules": [],
"default_tags": {
"expense": ["开销"],
"task_done": ["任务", "完成"],
"task_todo": ["任务", "待办"],
"schedule": ["日程"],
"idea": ["灵感"],
},
"hints": {
"idea": r"想到|想法|灵感|也许可以|可以做|或许可以|点子|构思|试试",
"schedule": r"明天|后天|今天|今晚|上午|中午|下午|晚上|点|会议|开会|体检|预约|去|安排|计划",
"task_done": r"完成|做完|改完|整理好|整理了|收拾好|收拾了|交了|打了|写完|提交了|处理了|跑了",
"task_todo": r"待办|要做|需要|得|准备|记得|安排",
"expense": r"花了|花费|消费|买|付款|支付|转账|报销|元|块|¥|¥|人民币",
},
}
CONFIG = None
def load_config(path: Optional[str]) -> Dict:
default_path = Path(__file__).resolve().parents[1] / 'references' / 'parser_config.json'
config_path = Path(path) if path else default_path
if not config_path.exists():
return DEFAULT_CONFIG
with open(config_path, 'r', encoding='utf-8') as f:
loaded = json.load(f)
merged = dict(DEFAULT_CONFIG)
for key, value in loaded.items():
merged[key] = value
return merged
def split_items(text: str) -> List[str]:
normalized = re.sub(r"[\n;;。]+", "|", text)
normalized = re.sub(r",(?=(明天|后天|今天|今晚|上午|中午|下午|晚上|想到|想法|灵感|买|花了|花费|完成|做完|改完|整理好|交了|打了|写完|提交了|处理了))", "|", normalized)
normalized = re.sub(r"(?<=\d)\s+(?=(明天|后天|今天|今晚|上午|中午|下午|晚上|想到|想法|灵感|买|花了|花费|完成|做完|改完|整理好|交了|打了|写完|提交了|处理了))", "|", normalized)
parts = [p.strip(" ,,|\t") for p in normalized.split("|")]
return [p for p in parts if p]
def infer_type(text: str) -> str:
hints = CONFIG['hints']
if re.search(hints['idea'], text):
return 'idea'
if re.search(hints['expense'], text) and re.search(r"\d+(?:\.\d+)?", text):
return 'expense'
if re.search(hints['task_done'], text):
return 'task'
if re.search(hints['task_todo'], text):
return 'task'
if re.search(hints['schedule'], text):
if re.search(r"明天|后天|今天|今晚|周[一二三四五六日天]|星期[一二三四五六日天]|[零一二两三四五六七八九十\d]{1,3}点|:\d{2}", text):
return 'schedule'
return 'idea'
CN_NUM = {"零":0,"一":1,"二":2,"两":2,"三":3,"四":4,"五":5,"六":6,"七":7,"八":8,"九":9,"十":10}
def cn_to_int(token: str) -> Optional[int]:
if token.isdigit():
return int(token)
if token in CN_NUM:
return CN_NUM[token]
if token.startswith('十'):
return 10 + CN_NUM.get(token[1:], 0)
if '十' in token:
left, right = token.split('十', 1)
left_val = CN_NUM.get(left, 1)
right_val = CN_NUM.get(right, 0)
return left_val * 10 + right_val
return None
def parse_time(text: str) -> Tuple[Optional[str], Optional[str], Optional[str]]:
meridiem = None
if '上午' in text:
meridiem = '上午'
elif '中午' in text:
meridiem = '中午'
elif '下午' in text:
meridiem = '下午'
elif '晚上' in text or '今晚' in text:
meridiem = '晚上'
match = re.search(r"([零一二两三四五六七八九十\d]{1,3})(?::|点)(\d{1,2})?", text)
if not match:
return None, None, None
hour = cn_to_int(match.group(1))
if hour is None:
return None, meridiem, match.group(0)
minute = int(match.group(2) or 0)
if meridiem in ('下午', '晚上') and hour < 12:
hour += 12
if meridiem == '中午' and hour < 11:
hour += 12
return f"{hour:02d}:{minute:02d}", meridiem, match.group(0)
def parse_date_value(text: str, today: date) -> str:
if '后天' in text:
return (today + timedelta(days=2)).isoformat()
if '明天' in text:
return (today + timedelta(days=1)).isoformat()
if '今天' in text or '今晚' in text:
return today.isoformat()
m = re.search(r"(\d{4})-(\d{1,2})-(\d{1,2})", text)
if m:
return f"{int(m.group(1)):04d}-{int(m.group(2)):02d}-{int(m.group(3)):02d}"
m = re.search(r"(\d{1,2})月(\d{1,2})日", text)
if m:
return f"{today.year:04d}-{int(m.group(1)):02d}-{int(m.group(2)):02d}"
m = re.search(r"(?:周|星期)([一二三四五六日天])", text)
if m:
target = WEEKDAY_MAP[m.group(1)]
delta = (target - today.weekday()) % 7
return (today + timedelta(days=delta)).isoformat()
return today.isoformat()
def next_sequence(db_path: Optional[Path], record_type: str, datestr: str, used: Dict[Tuple[str, str], int]) -> int:
key = (record_type, datestr)
current = used.get(key, 0)
if current:
used[key] = current + 1
return current + 1
max_seq = 0
if db_path and db_path.exists():
prefix = TYPE_PREFIX[record_type]
ymd = datestr.replace('-', '')
pattern = f"{prefix}_{ymd}_%"
conn = sqlite3.connect(str(db_path))
try:
rows = conn.execute('SELECT id FROM entries WHERE id LIKE ?', (pattern,)).fetchall()
except sqlite3.Error:
rows = []
finally:
conn.close()
for (entry_id,) in rows:
m = re.match(r"^[a-z]+_\d{8}_(\d{3})$", entry_id)
if m:
max_seq = max(max_seq, int(m.group(1)))
used[key] = max_seq + 1
return max_seq + 1
def make_id(db_path: Optional[Path], record_type: str, datestr: str, used: Dict[Tuple[str, str], int]) -> str:
seq = next_sequence(db_path, record_type, datestr, used)
return f"{TYPE_PREFIX[record_type]}_{datestr.replace('-', '')}_{seq:03d}"
def dedupe_tags(tags: List[str]) -> List[str]:
out = []
for t in tags:
t = (t or '').strip().replace('#', '')
if t and t not in out:
out.append(t[:8])
return out[:4]
def apply_first_rule(text: str, rules: List[Dict], default_key: str, default_value: Optional[str]) -> Tuple[Optional[str], List[str]]:
for rule in rules:
if re.search(rule['pattern'], text):
return rule.get(default_key, default_value), rule.get('tags', [])
return default_value, []
def parse_expense(text: str) -> Dict:
amount = None
m = re.search(r"(\d+(?:\.\d+)?)\s*(?:元|块|¥|¥)", text)
if not m:
m = re.search(r"(?:花了|花费|消费|买(?:了)?)[^\d]*(\d+(?:\.\d+)?)", text)
if m:
amount = float(m.group(1))
if amount.is_integer():
amount = int(amount)
category, category_tags = apply_first_rule(text, CONFIG['expense_category_rules'], 'category', '其他')
subcategory, sub_tags = apply_first_rule(text, CONFIG['expense_subcategory_rules'], 'subcategory', None)
merchant = None
merchant_match = re.search(r"在(.+?)(?:买|吃|付款|消费)", text)
if merchant_match:
merchant = merchant_match.group(1).strip()
pay_method = None
if '微信' in text:
pay_method = '微信'
elif '支付宝' in text:
pay_method = '支付宝'
elif '银行卡' in text:
pay_method = '银行卡'
elif '现金' in text:
pay_method = '现金'
tags = CONFIG['default_tags'].get('expense', []) + [category] + category_tags + sub_tags
if subcategory:
tags.append(subcategory)
tags = dedupe_tags(tags)
summary = text
if amount is not None:
summary = f"{category}消费 {amount} 元"
if subcategory:
summary = f"{subcategory},消费 {amount} 元"
return {
'tags': tags,
'summary': summary,
'payload': {
'amount': amount,
'currency': 'CNY' if amount is not None else None,
'category': category,
'subcategory': subcategory,
'merchant': merchant,
'pay_method': pay_method,
},
}
def parse_task(text: str, today: date) -> Dict:
status = 'done' if re.search(CONFIG['hints']['task_done'], text) else 'todo'
priority = 'high' if re.search(r"重要|尽快|马上|紧急", text) else 'normal'
due_date = None
if status != 'done' and re.search(r"明天|后天|今天|周[一二三四五六日天]|星期[一二三四五六日天]|\d{4}-\d{2}-\d{2}|\d{1,2}月\d{1,2}日", text):
due_date = parse_date_value(text, today)
completed_at = datetime.now().strftime('%Y-%m-%dT%H:%M:%S') if status == 'done' else None
project, project_tags = apply_first_rule(text, CONFIG['task_project_rules'], 'project', None)
tags = CONFIG['default_tags'].get('task_done' if status == 'done' else 'task_todo', []) + project_tags
if project:
tags.append(project)
tags = dedupe_tags(tags)
return {
'tags': tags,
'summary': text,
'payload': {
'status': status,
'priority': priority,
'project': project,
'due_date': due_date,
'completed_at': completed_at,
},
}
def parse_schedule(text: str, today: date) -> Dict:
schedule_date = parse_date_value(text, today)
start_time, meridiem, _ = parse_time(text)
end_time = None
m = re.search(r"(\d{1,2})(?::|点)(\d{1,2})?\s*[到-]\s*(\d{1,2})(?::|点)?(\d{1,2})?", text)
if m:
sh = int(m.group(1))
sm = int(m.group(2) or 0)
eh = int(m.group(3))
em = int(m.group(4) or 0)
if meridiem in ('下午', '晚上') and sh < 12:
sh += 12
if meridiem in ('下午', '晚上') and eh < 12:
eh += 12
start_time = f"{sh:02d}:{sm:02d}"
end_time = f"{eh:02d}:{em:02d}"
location = None
loc = re.search(r"去([^,,。]+)", text)
if loc:
location = loc.group(1).strip()
status = 'planned'
if '已完成' in text or '去了' in text:
status = 'done'
elif '取消' in text:
status = 'skipped'
tags = list(CONFIG['default_tags'].get('schedule', ['日程']))
extra_tag = None
for rule in CONFIG['schedule_tag_rules']:
if re.search(rule['pattern'], text):
extra_tag = rule['tag']
break
if extra_tag:
tags.append(extra_tag)
elif location:
tags.append(location[:6])
tags = dedupe_tags(tags)
return {
'tags': tags,
'summary': text,
'payload': {
'schedule_date': schedule_date,
'start_time': start_time,
'end_time': end_time,
'location': location,
'status': status,
},
}
def parse_idea(text: str) -> Dict:
idea_type, idea_tags = apply_first_rule(text, CONFIG['idea_type_rules'], 'idea_type', '生活')
tags = dedupe_tags(CONFIG['default_tags'].get('idea', ['灵感']) + idea_tags + [idea_type])
return {
'tags': tags,
'summary': text,
'payload': {
'idea_type': idea_type,
'status': 'captured',
'related_task_id': None,
},
}
def parse_record(text: str, today: date, db_path: Optional[Path], used: Dict[Tuple[str, str], int]) -> Dict:
rtype = infer_type(text)
effective_date = parse_date_value(text, today) if rtype == 'schedule' else today.isoformat()
time_value, _, _ = parse_time(text)
parser = {
'expense': parse_expense,
'task': lambda t: parse_task(t, today),
'schedule': lambda t: parse_schedule(t, today),
'idea': parse_idea,
}[rtype]
parsed = parser(text)
return {
'id': make_id(db_path, rtype, effective_date, used),
'type': rtype,
'date': effective_date if rtype != 'schedule' else parsed['payload'].get('schedule_date') or effective_date,
'time': time_value,
'title': None,
'raw_text': text,
'summary': parsed['summary'],
'tags': parsed['tags'],
'payload': parsed['payload'],
}
def main() -> int:
global CONFIG
parser = argparse.ArgumentParser()
parser.add_argument('--text')
parser.add_argument('--input')
parser.add_argument('--stdin', action='store_true')
parser.add_argument('--db')
parser.add_argument('--today', help='YYYY-MM-DD')
parser.add_argument('--config', help='path to parser config json')
args = parser.parse_args()
CONFIG = load_config(args.config)
if args.stdin:
import sys
text = sys.stdin.read().strip()
elif args.input:
text = Path(args.input).read_text(encoding='utf-8').strip()
elif args.text:
text = args.text.strip()
else:
raise SystemExit('provide --text, --input, or --stdin')
today = date.fromisoformat(args.today) if args.today else date.today()
db_path = Path(args.db) if args.db else None
items = split_items(text)
used: Dict[Tuple[str, str], int] = defaultdict(int)
records = [parse_record(item, today, db_path, used) for item in items]
print(json.dumps({'records': records}, ensure_ascii=False, indent=2))
return 0
if __name__ == '__main__':
raise SystemExit(main())
```
### references/parser_config.json
```json
{
"expense_category_rules": [
{
"pattern": "咖啡|奶茶|早餐|午餐|晚餐|吃|牛肉面|面|饭|外卖|便利店|饮料",
"category": "饮食",
"subcategory": null,
"tags": [
"饮食"
]
},
{
"pattern": "地铁|公交|打车|滴滴|高铁|火车|机票|加油|停车",
"category": "交通",
"subcategory": null,
"tags": [
"交通"
]
},
{
"pattern": "药|医院|看牙|体检|挂号",
"category": "医疗",
"subcategory": null,
"tags": [
"医疗"
]
},
{
"pattern": "书|课程|学费|培训",
"category": "学习",
"subcategory": null,
"tags": [
"学习"
]
},
{
"pattern": "电影|游戏|KTV|演出",
"category": "娱乐",
"subcategory": null,
"tags": [
"娱乐"
]
},
{
"pattern": "房租|水费|电费|燃气|物业|家居",
"category": "居家",
"subcategory": null,
"tags": [
"居家"
]
},
{
"pattern": "礼物|聚餐|请客",
"category": "社交",
"subcategory": null,
"tags": [
"社交"
]
},
{
"pattern": "衣服|鞋|裤|耳机|手机|电脑|淘宝|京东|买了",
"category": "购物",
"subcategory": null,
"tags": [
"购物"
]
}
],
"expense_subcategory_rules": [
{
"pattern": "咖啡",
"subcategory": "咖啡",
"tags": [
"咖啡"
]
},
{
"pattern": "牛肉面|面",
"subcategory": "面食",
"tags": [
"面食"
]
},
{
"pattern": "早餐",
"subcategory": "早餐",
"tags": [
"早餐"
]
},
{
"pattern": "午餐|中午",
"subcategory": "午饭",
"tags": [
"午饭"
]
},
{
"pattern": "晚餐|晚上",
"subcategory": "晚饭",
"tags": [
"晚饭"
]
}
],
"idea_type_rules": [
{
"pattern": "写|文章|博客|文案",
"idea_type": "写作",
"tags": [
"写作"
]
},
{
"pattern": "系统|数据看板|自动化|模板|流程",
"idea_type": "系统",
"tags": [
"系统"
]
},
{
"pattern": "项目|产品|功能|应用",
"idea_type": "产品",
"tags": [
"产品"
]
}
],
"task_project_rules": [
{
"pattern": "合同|报告|文档",
"project": "文档",
"tags": [
"文档"
]
},
{
"pattern": "书桌|阳台|房间|桌面|家务",
"project": "家务",
"tags": [
"家务"
]
},
{
"pattern": "跑步|健身|运动",
"project": "健康",
"tags": [
"健康"
]
}
],
"schedule_tag_rules": [
{
"pattern": "会议|开会",
"tag": "会议"
},
{
"pattern": "体检|医院|看牙",
"tag": "健康"
},
{
"pattern": "银行|报销",
"tag": "事务"
}
],
"default_tags": {
"expense": [
"开销"
],
"task_done": [
"任务",
"完成"
],
"task_todo": [
"任务",
"待办"
],
"schedule": [
"日程"
],
"idea": [
"灵感"
]
},
"hints": {
"idea": "想到|想法|灵感|也许可以|可以做|或许可以|点子|构思|试试",
"schedule": "明天|后天|今天|今晚|上午|中午|下午|晚上|点|会议|开会|体检|预约|去|安排|计划",
"task_done": "完成|做完|改完|整理好|整理了|收拾好|收拾了|交了|打了|写完|提交了|处理了|跑了",
"task_todo": "待办|要做|需要|得|准备|记得|安排",
"expense": "花了|花费|消费|买|付款|支付|转账|报销|元|块|¥|¥|人民币"
}
}
```
### scripts/process_entry.py
```python
#!/usr/bin/env python3
import argparse
import json
import subprocess
import sys
from pathlib import Path
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--root", required=True)
parser.add_argument("--db", required=True)
parser.add_argument("--text")
parser.add_argument("--input")
parser.add_argument("--stdin", action="store_true")
parser.add_argument("--today")
parser.add_argument("--config")
args = parser.parse_args()
if args.stdin:
text = sys.stdin.read().strip()
elif args.input:
text = Path(args.input).read_text(encoding="utf-8").strip()
elif args.text:
text = args.text.strip()
else:
raise SystemExit("provide --text, --input, or --stdin")
script_dir = Path(__file__).resolve().parent
parse_cmd = [sys.executable, str(script_dir / "parse_entries.py"), "--text", text, "--db", args.db]
if args.today:
parse_cmd.extend(["--today", args.today])
if args.config:
parse_cmd.extend(["--config", args.config])
parsed = subprocess.run(parse_cmd, check=True, capture_output=True, text=True)
payload = parsed.stdout
db_path = Path(args.db)
if not db_path.exists():
init_cmd = [sys.executable, str(script_dir / "init_db.py"), "--db", args.db]
subprocess.run(init_cmd, check=True)
save_cmd = [sys.executable, str(script_dir / "save_entry.py"), "--root", args.root, "--db", args.db, "--stdin-json"]
saved = subprocess.run(save_cmd, input=payload, check=True, capture_output=True, text=True)
parsed_obj = json.loads(payload)
saved_obj = json.loads(saved.stdout)
print(json.dumps({"parsed": parsed_obj["records"], "saved": saved_obj["saved"]}, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
```
### scripts/save_entry.py
```python
#!/usr/bin/env python3
import argparse
import json
import sqlite3
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional
SECTION_TITLES = {
"expense": "开销",
"task": "任务",
"schedule": "日程",
"idea": "灵感",
}
TYPE_PREFIX = {
"expense": "exp",
"task": "task",
"schedule": "sched",
"idea": "idea",
}
@dataclass
class SaveResult:
id: str
type: str
md_file: str
markdown_block: str
db_written: bool
def ensure_daily_file(path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
if not path.exists():
path.write_text(f"# {path.stem}\n\n", encoding="utf-8")
def ensure_section(text: str, title: str) -> str:
header = f"## {title}\n"
if header in text:
return text
if not text.endswith("\n"):
text += "\n"
return text + f"\n{header}\n"
def format_value(v: Optional[object]) -> str:
if v is None or v == "":
return ""
return str(v)
def build_block(record: Dict) -> str:
tags = " ".join(f"#{t}" for t in record.get("tags", []))
lines = [
f"### {record['id']}",
f"- 时间:{format_value(record.get('time'))}",
f"- 标签:{tags}",
f"- 原始描述:{record.get('raw_text', '')}",
f"- 摘要:{record.get('summary', '')}",
]
payload = record.get("payload", {}) or {}
rtype = record["type"]
if rtype == "expense":
lines.extend([
f"- 金额:{format_value(payload.get('amount'))}",
f"- 币种:{format_value(payload.get('currency'))}",
f"- 分类:{format_value(payload.get('category'))}",
f"- 子分类:{format_value(payload.get('subcategory'))}",
f"- 商家:{format_value(payload.get('merchant'))}",
f"- 支付方式:{format_value(payload.get('pay_method'))}",
])
elif rtype == "task":
lines.extend([
f"- 状态:{format_value(payload.get('status'))}",
f"- 优先级:{format_value(payload.get('priority'))}",
f"- 项目:{format_value(payload.get('project'))}",
f"- 截止日期:{format_value(payload.get('due_date'))}",
f"- 完成时间:{format_value(payload.get('completed_at'))}",
])
elif rtype == "schedule":
lines.extend([
f"- 日期:{format_value(payload.get('schedule_date'))}",
f"- 开始时间:{format_value(payload.get('start_time'))}",
f"- 结束时间:{format_value(payload.get('end_time'))}",
f"- 地点:{format_value(payload.get('location'))}",
f"- 状态:{format_value(payload.get('status'))}",
])
elif rtype == "idea":
lines.extend([
f"- 类型:{format_value(payload.get('idea_type'))}",
f"- 状态:{format_value(payload.get('status'))}",
f"- 关联任务:{format_value(payload.get('related_task_id'))}",
])
return "\n".join(lines) + "\n\n"
def upsert_entry(conn: sqlite3.Connection, record: Dict, md_file: str) -> None:
now = datetime.now().isoformat(timespec="seconds")
payload_json = json.dumps(record.get("payload", {}), ensure_ascii=False)
conn.execute(
"""
INSERT INTO entries (
id, type, date, time, title, raw_text, summary, source, md_file,
md_anchor, payload_json, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
type=excluded.type,
date=excluded.date,
time=excluded.time,
title=excluded.title,
raw_text=excluded.raw_text,
summary=excluded.summary,
source=excluded.source,
md_file=excluded.md_file,
md_anchor=excluded.md_anchor,
payload_json=excluded.payload_json,
updated_at=excluded.updated_at
""",
(
record["id"],
record["type"],
record["date"],
record.get("time"),
record.get("title"),
record.get("raw_text", ""),
record.get("summary"),
record.get("source", "chat"),
md_file,
record["id"],
payload_json,
now,
now,
),
)
def replace_child(conn: sqlite3.Connection, table: str, entry_id: str, fields: Dict[str, object]) -> None:
conn.execute(f"DELETE FROM {table} WHERE entry_id = ?", (entry_id,))
columns = ["entry_id"] + list(fields.keys())
values = [entry_id] + [fields[k] for k in fields.keys()]
placeholders = ", ".join(["?"] * len(columns))
col_sql = ", ".join(columns)
conn.execute(f"INSERT INTO {table} ({col_sql}) VALUES ({placeholders})", values)
def sync_tags(conn: sqlite3.Connection, entry_id: str, tags: List[str]) -> None:
conn.execute("DELETE FROM entry_tags WHERE entry_id = ?", (entry_id,))
for tag in tags:
conn.execute("INSERT OR IGNORE INTO tags (name) VALUES (?)", (tag,))
tag_id = conn.execute("SELECT id FROM tags WHERE name = ?", (tag,)).fetchone()[0]
conn.execute(
"INSERT OR IGNORE INTO entry_tags (entry_id, tag_id) VALUES (?, ?)",
(entry_id, tag_id),
)
def append_markdown(root: Path, record: Dict) -> (str, str):
daily_file = root / "daily" / f"{record['date']}.md"
ensure_daily_file(daily_file)
text = daily_file.read_text(encoding="utf-8")
section = SECTION_TITLES[record["type"]]
text = ensure_section(text, section)
block = build_block(record)
marker = f"### {record['id']}\n"
if marker in text:
start = text.index(marker)
next_pos = text.find("\n### ", start + len(marker))
if next_pos == -1:
next_pos = len(text)
text = text[:start] + block + text[next_pos:]
else:
header = f"## {section}\n"
insert_at = text.index(header) + len(header)
text = text[:insert_at] + "\n" + block + text[insert_at:]
daily_file.write_text(text, encoding="utf-8")
return str(daily_file), block
def save_records(root: Path, db_path: Path, records: List[Dict]) -> List[SaveResult]:
db_path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(str(db_path))
conn.execute("PRAGMA foreign_keys = ON")
results: List[SaveResult] = []
try:
for record in records:
md_file, block = append_markdown(root, record)
upsert_entry(conn, record, md_file)
payload = record.get("payload", {}) or {}
if record["type"] == "expense":
replace_child(conn, "expenses", record["id"], {
"amount": payload.get("amount"),
"currency": payload.get("currency"),
"category": payload.get("category"),
"subcategory": payload.get("subcategory"),
"pay_method": payload.get("pay_method"),
"merchant": payload.get("merchant"),
})
elif record["type"] == "task":
replace_child(conn, "tasks", record["id"], {
"status": payload.get("status"),
"priority": payload.get("priority"),
"project": payload.get("project"),
"due_date": payload.get("due_date"),
"completed_at": payload.get("completed_at"),
})
elif record["type"] == "schedule":
replace_child(conn, "schedules", record["id"], {
"schedule_date": payload.get("schedule_date"),
"start_time": payload.get("start_time"),
"end_time": payload.get("end_time"),
"location": payload.get("location"),
"status": payload.get("status"),
})
elif record["type"] == "idea":
replace_child(conn, "ideas", record["id"], {
"idea_type": payload.get("idea_type"),
"status": payload.get("status"),
"related_task_id": payload.get("related_task_id"),
})
sync_tags(conn, record["id"], record.get("tags", []))
results.append(SaveResult(record["id"], record["type"], md_file, block, True))
conn.commit()
finally:
conn.close()
return results
def load_payload(args: argparse.Namespace) -> Dict:
if args.stdin_json:
return json.load(__import__("sys").stdin)
if args.input:
with open(args.input, "r", encoding="utf-8") as f:
return json.load(f)
raise SystemExit("provide --stdin-json or --input")
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--root", required=True)
parser.add_argument("--db", required=True)
parser.add_argument("--stdin-json", action="store_true")
parser.add_argument("--input")
args = parser.parse_args()
payload = load_payload(args)
records = payload.get("records") or []
if not isinstance(records, list) or not records:
raise SystemExit("payload.records must be a non-empty list")
results = save_records(Path(args.root), Path(args.db), records)
print(json.dumps({
"saved": [r.__dict__ for r in results]
}, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
```
### scripts/init_db.py
```python
#!/usr/bin/env python3
import argparse
import sqlite3
from pathlib import Path
SCHEMA = """
CREATE TABLE IF NOT EXISTS entries (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
date TEXT NOT NULL,
time TEXT,
title TEXT,
raw_text TEXT NOT NULL,
summary TEXT,
source TEXT DEFAULT 'chat',
md_file TEXT,
md_anchor TEXT,
payload_json TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS expenses (
entry_id TEXT PRIMARY KEY,
amount REAL,
currency TEXT,
category TEXT,
subcategory TEXT,
pay_method TEXT,
merchant TEXT,
FOREIGN KEY(entry_id) REFERENCES entries(id)
);
CREATE TABLE IF NOT EXISTS tasks (
entry_id TEXT PRIMARY KEY,
status TEXT,
priority TEXT,
project TEXT,
due_date TEXT,
completed_at TEXT,
FOREIGN KEY(entry_id) REFERENCES entries(id)
);
CREATE TABLE IF NOT EXISTS schedules (
entry_id TEXT PRIMARY KEY,
schedule_date TEXT,
start_time TEXT,
end_time TEXT,
location TEXT,
status TEXT,
FOREIGN KEY(entry_id) REFERENCES entries(id)
);
CREATE TABLE IF NOT EXISTS ideas (
entry_id TEXT PRIMARY KEY,
idea_type TEXT,
status TEXT,
related_task_id TEXT,
FOREIGN KEY(entry_id) REFERENCES entries(id)
);
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS entry_tags (
entry_id TEXT NOT NULL,
tag_id INTEGER NOT NULL,
PRIMARY KEY (entry_id, tag_id),
FOREIGN KEY(entry_id) REFERENCES entries(id),
FOREIGN KEY(tag_id) REFERENCES tags(id)
);
"""
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--db", required=True)
args = parser.parse_args()
db_path = Path(args.db)
db_path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(str(db_path))
try:
conn.executescript(SCHEMA)
conn.commit()
finally:
conn.close()
print(f"initialized database at {db_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
```
### references/schema.md
```markdown
# Schema
The skill uses one index table plus type-specific child tables.
## entries
- `id`
- `type`
- `date`
- `time`
- `title`
- `raw_text`
- `summary`
- `source`
- `md_file`
- `md_anchor`
- `payload_json`
- `created_at`
- `updated_at`
## expenses
- `entry_id`
- `amount`
- `currency`
- `category`
- `subcategory`
- `pay_method`
- `merchant`
## tasks
- `entry_id`
- `status`
- `priority`
- `project`
- `due_date`
- `completed_at`
## schedules
- `entry_id`
- `schedule_date`
- `start_time`
- `end_time`
- `location`
- `status`
## ideas
- `entry_id`
- `idea_type`
- `status`
- `related_task_id`
## tags
- `id`
- `name`
## entry_tags
- `entry_id`
- `tag_id`
```
### references/examples.md
```markdown
# Example payloads and commands
## One-command end-to-end write
```bash
python scripts/process_entry.py --root life --db life/db/life.db --today 2026-03-10 --text "今天中午吃牛肉面花了 26 元,明天下午两点去体检,想到可以做一个生活数据看板"
```
## Parse only
```bash
python scripts/parse_entries.py --text "买咖啡 18 元,改完合同,明天下午两点去体检"
```
## Single expense payload
```json
{
"records": [
{
"id": "exp_20260310_001",
"type": "expense",
"date": "2026-03-10",
"time": "12:30",
"raw_text": "今天中午吃牛肉面花了 26 元",
"summary": "午饭,消费 26 元",
"tags": ["开销", "饮食", "午饭"],
"payload": {
"amount": 26,
"currency": "CNY",
"category": "饮食",
"subcategory": "午饭"
}
}
]
}
```
## Mixed input split into multiple records
```json
{
"records": [
{
"id": "exp_20260310_001",
"type": "expense",
"date": "2026-03-10",
"time": null,
"raw_text": "买咖啡 18 元",
"summary": "咖啡,消费 18 元",
"tags": ["开销", "饮食", "咖啡"],
"payload": {
"amount": 18,
"currency": "CNY",
"category": "饮食",
"subcategory": "咖啡"
}
},
{
"id": "task_20260310_001",
"type": "task",
"date": "2026-03-10",
"time": null,
"raw_text": "改完合同",
"summary": "改完合同",
"tags": ["任务", "完成", "文档"],
"payload": {
"status": "done",
"priority": "normal",
"project": "文档",
"due_date": null,
"completed_at": "2026-03-10T10:00:00"
}
},
{
"id": "sched_20260311_001",
"type": "schedule",
"date": "2026-03-11",
"time": "14:00",
"raw_text": "明天下午两点去体检",
"summary": "明天下午两点去体检",
"tags": ["日程", "健康"],
"payload": {
"schedule_date": "2026-03-11",
"start_time": "14:00",
"end_time": null,
"location": "体检",
"status": "planned"
}
}
]
}
```
## Parse with custom config
```bash
python scripts/parse_entries.py --config references/parser_config.json --text "买咖啡 18 元,整理了书桌,想到做周复盘模板"
```
## Stable relative dates
```bash
python scripts/parse_entries.py --today 2026-03-10 --text "明天下午两点去体检"
```
```
### references/configuration.md
```markdown
# Parser configuration
The parser behavior is controlled by `references/parser_config.json`.
## What you can change without editing Python
- expense category matching
- expense subcategory matching
- default tags
- task project mapping
- idea type mapping
- schedule tag mapping
- high-level hint regexes used during type inference
## Important notes
- Rules are evaluated top to bottom. Put more specific rules before broader ones.
- Each rule uses a Python regular expression string in `pattern`.
- Keep tags short. The parser trims tags to 8 characters and keeps at most 4 tags.
- When changing a pattern, test with `python scripts/parse_entries.py --text "..."` before packaging.
## File overview
### `expense_category_rules`
Maps text to an expense category and optional extra tags.
### `expense_subcategory_rules`
Adds a subcategory and optional extra tags.
### `idea_type_rules`
Maps an idea to a configured idea type.
### `task_project_rules`
Maps task text to a project bucket like `家务` or `健康`.
### `schedule_tag_rules`
Adds one extra tag for schedule entries.
### `default_tags`
Defines default tags for each record type.
### `hints`
Regexes used for initial type inference.
```
---
## Skill Companion Files
> Additional files collected from the skill directory layout.
### _meta.json
```json
{
"owner": "epitomizelu",
"slug": "life-capture",
"displayName": "Life Capture",
"latest": {
"version": "1.0.0",
"publishedAt": 1773103111057,
"commit": "https://github.com/openclaw/skills/commit/f561f4b967a0c5db6036c2484cd09392c45234ec"
},
"history": []
}
```