Birthday
管理組織成員(家人、同事、親戚等)的個人資料、農曆/新曆生日及紀念日,並用 OpenClaw cron 設置自動提醒。Use when: (1) 添加/編輯成員資料, (2) 設置生日或紀念日提醒, (3) 更新農曆對應的新曆日期, (4) 查看即將到來的重要日子, (5) 管理多個組織的成員。NOT for: 一般日程安排(用 calendar skill)。
安装 / 下载方式
TotalClaw CLI推荐
totalclaw install clawskills:ifeychan702~mybirthdaycURL直接下载,无需登录
curl -fsSL https://skills.taituai.com/api/skills/clawskills%3Aifeychan702~mybirthday/file -o mybirthday.mdGit 仓库获取源码
git clone https://github.com/openclaw/skills/commit/12ec4b2d852512ec38864dc893c13bd987a914c7# member-manager — 成員資料與生日紀念日管理
管理多個組織(家庭、同事、親戚等)中成員的個人資料、農曆/新曆生日及各類紀念日,並透過 OpenClaw scheduled-tasks 自動發送提醒。
---
## 核心原則
1. **先讀後寫**:任何修改操作前,先讀取現有 JSON 檔案內容
2. **原子寫入**:每次寫入完整 JSON,不做部分更新
3. **用戶確認**:添加/刪除成員前向用戶確認資訊是否正確
4. **農曆自動轉換**:用戶提供農曆日期時,自動計算當年新曆日期
5. **提醒同步**:刪除成員時同步清理提醒;修改生日時同步更新提醒
---
## 多用戶數據隔離
此 skill 支持多用戶共享同一個 OpenClaw(例如 Telegram bot 場景)。每個用戶的數據存放在以**用戶 session key 命名的獨立目錄**,互不干擾。
### 數據路徑規則
```
~/.openclaw/workspace/users/{SESSION_KEY}/members.json
~/.openclaw/workspace/users/{SESSION_KEY}/reminders.json
```
`{SESSION_KEY}` 是 OpenClaw 的當前 session 標識符(Telegram DM 場景即為 Telegram user ID)。
**在使用任何成員數據前,必須先確定當前用戶的路徑。**
### 如何確定當前用戶的數據目錄
讀取環境變數或 session context 取得用戶 ID,**必須先清洗以防止目錄遍歷攻擊**,然後初始化數據目錄:
```python
import os, json, re
def sanitize_session_key(raw_key: str) -> str:
"""清洗 session key,只允許字母數字、連字號和底線,防止目錄遍歷"""
sanitized = re.sub(r'[^a-zA-Z0-9_-]', '', raw_key)
if not sanitized:
raise ValueError(f"Invalid session key: {raw_key!r}")
return sanitized
# 方法 1:從環境變數取得
raw_id = os.environ.get("OPENCLAW_SESSION_KEY", "default")
# 方法 2:如果是在 Telegram bot 場景,嘗試讀取 session 檔案
session_file = os.path.expanduser("~/.openclaw/session.json")
if os.path.exists(session_file):
with open(session_file) as f:
session = json.load(f)
raw_id = session.get("user_id", session.get("session_key", "default"))
user_id = sanitize_session_key(str(raw_id))
WORKSPACE_ROOT = os.path.expanduser("~/.openclaw/workspace/users")
base_dir = os.path.join(WORKSPACE_ROOT, user_id)
# 二次驗證:確保解析後的路徑仍在 workspace 目錄內
real_base = os.path.realpath(base_dir)
real_root = os.path.realpath(WORKSPACE_ROOT)
if not real_base.startswith(real_root + os.sep):
raise ValueError(f"Path traversal detected: {base_dir}")
members_path = os.path.join(base_dir, "members.json")
reminders_path = os.path.join(base_dir, "reminders.json")
# 初始化新用戶
os.makedirs(base_dir, exist_ok=True)
if not os.path.exists(members_path):
with open(members_path, "w") as f:
json.dump([], f)
if not os.path.exists(reminders_path):
with open(reminders_path, "w") as f:
json.dump([], f)
```
---
## 數據結構
### members.json
```json
[
{
"id": "uuid-v4",
"name": "張三",
"group": "家庭",
"relationship": "父親",
"phone": "0912345678",
"birthday_solar": "1955-03-15",
"birthday_lunar": {
"month": 2,
"day": 13,
"is_leap_month": false
},
"birthday_lunar_solar_this_year": "2025-03-12",
"anniversaries": [
{
"id": "uuid-v4",
"label": "結婚紀念日",
"date_solar": "1980-06-20",
"recurring": true
}
],
"notes": "喜歡喝普洱茶",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
]
```
欄位說明:
- `id`:UUID v4,成員唯一標識
- `name`:姓名(必填)
- `group`:組別/組織名稱,如「家庭」「公司」「大學同學」(必填)
- `relationship`:與用戶的關係,如「父親」「主管」「同學」
- `phone`:電話號碼
- `birthday_solar`:新曆生日,格式 `YYYY-MM-DD`
- `birthday_lunar`:農曆生日,含 month/day/is_leap_month
- `birthday_lunar_solar_this_year`:農曆生日在**當年**對應的新曆日期(每年需更新)
- `anniversaries`:紀念日列表,每個紀念日有獨立 id
- `notes`:備註
- `created_at` / `updated_at`:時間戳
### reminders.json
```json
[
{
"id": "uuid-v4",
"member_id": "對應 member 的 id",
"type": "birthday_solar",
"label": "張三 新曆生日",
"advance_days": 3,
"scheduled_task_id": "member-birthday-張三-solar",
"enabled": true
}
]
```
欄位說明:
- `type`:`birthday_solar` | `birthday_lunar` | `anniversary`
- `advance_days`:提前幾天提醒(默認 3 天)
- `scheduled_task_id`:對應 OpenClaw scheduled-task 的 ID
- `enabled`:是否啟用
---
## 農曆處理
農曆轉新曆每年都會變,因此需要**每年更新**農曆生日對應的新曆日期。
### 農曆轉新曆
```python
# pip install lunardate
from lunardate import LunarDate
from datetime import date, timedelta
def lunar_to_solar(year: int, lunar_month: int, lunar_day: int, is_leap: bool = False) -> date:
"""將農曆日期轉換為新曆日期"""
try:
lunar = LunarDate(year, lunar_month, lunar_day, is_leap)
return lunar.toSolarDate()
except ValueError:
# 若該年無此農曆日期(例如閏月不存在),嘗試非閏月
if is_leap:
lunar = LunarDate(year, lunar_month, lunar_day, False)
return lunar.toSolarDate()
raise
```
### 計算提醒日期(正確處理跨月)
```python
def calculate_reminder_date(target_date: date, advance_days: int) -> date:
"""計算提前提醒的日期,正確處理跨月情況"""
return target_date - timedelta(days=advance_days)
```
### 年度更新所有農曆生日
```python
def update_all_lunar_birthdays(base_dir: str, year: int):
"""更新所有成員農曆生日在指定年份的新曆日期"""
members_path = os.path.join(base_dir, "members.json")
with open(members_path) as f:
members = json.load(f)
updated = []
for m in members:
lunar = m.get("birthday_lunar")
if lunar:
try:
solar = lunar_to_solar(
year, lunar["month"], lunar["day"],
lunar.get("is_leap_month", False)
)
m["birthday_lunar_solar_this_year"] = str(solar)
updated.append(m["name"])
except ValueError as e:
# 記錄錯誤但繼續處理其他成員
pass
m["updated_at"] = datetime.utcnow().isoformat() + "Z"
with open(members_path, "w") as f:
json.dump(members, f, ensure_ascii=False, indent=2)
return updated
```
---
## 功能操作指南
### 1. 添加成員
當用戶要求添加成員時:
1. 解析用戶提供的信息(姓名、組別、關係、生日等)
2. 如果用戶提供農曆生日,自動轉換為當年新曆日期
3. 向用戶確認信息後寫入
```python
import json, os, uuid
from datetime import datetime
def add_member(base_dir, name, group, relationship="", birthday_solar=None,
birthday_lunar=None, phone="", notes=""):
path = os.path.join(base_dir, "members.json")
with open(path) as f:
members = json.load(f)
# 檢查是否已有同名同組成員
for existing in members:
if existing["name"] == name and existing["group"] == group:
return None, f"已存在同名成員:{name}({group})"
now = datetime.utcnow().isoformat() + "Z"
member = {
"id": str(uuid.uuid4()),
"name": name,
"group": group,
"relationship": relationship,
"phone": phone,
"birthday_solar": birthday_solar,
"birthday_lunar": birthday_lunar,
"birthday_lunar_solar_this_year": None,
"anniversaries": [],
"notes": notes,
"created_at": now,
"updated_at": now
}
# 如果有農曆生日,計算當年新曆日期
if birthday_lunar:
from lunardate import LunarDate
from datetime import date
try:
solar = lunar_to_solar(
date.today().year,
birthday_lunar["month"],
birthday_lunar["day"],
birthday_lunar.get("is_leap_month", False)
)
member["birthday_lunar_solar_this_year"] = str(solar)
except ValueError:
pass
members.append(member)
with open(path, "w") as f:
json.dump(members, f, ensure_ascii=False, indent=2)
return member, None
```
### 2. 編輯成員
```python
def update_member(base_dir, member_id, **updates):
"""更新成員資料。支持更新任意欄位。"""
path = os.path.join(base_dir, "members.json")
with open(path) as f:
members = json.load(f)
for m in members:
if m["id"] == member_id:
for key, value in updates.items():
if key in m and key not in ("id", "created_at"):
m[key] = value
m["updated_at"] = datetime.utcnow().isoformat() + "Z"
# 如果更新了農曆生日,重新計算新曆日期
if "birthday_lunar" in updates and updates["birthday_lunar"]:
from datetime import date
try:
lunar = updates["birthday_lunar"]
solar = lunar_to_solar(
date.today().year,
lunar["month"], lunar["day"],
lunar.get("is_leap_month", False)
)
m["birthday_lunar_solar_this_year"] = str(solar)
except ValueError:
pass
with open(path, "w") as f:
json.dump(memb