Install any skill in seconds. Free to start, no credit card required.
Get Started Free →管理組織成員(家人、同事、親戚等)的個人資料、農曆/新曆生日及紀念日,並用 OpenClaw cron 設置自動提醒。Use when: (1) 添加/編輯成員資料, (2) 設置生日或紀念日提醒, (3) 更新農曆對應的新曆日期, (4) 查看即將到來的重要日子, (5) 管理多個組織的成員。NOT for: 一般日程安排(用 calendar skill)。
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-06 | ✗→✓ | ▲ Improved | 240% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 527% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 416% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 453% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 487% | 0% |
管理多個組織(家庭、同事、親戚等)中成員的個人資料、農曆/新曆生日及各類紀念日,並透過 OpenClaw scheduled-tasks 自動發送提醒。
此 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,必須先清洗以防止目錄遍歷攻擊,然後初始化數據目錄:
pythonimport 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)
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-DDbirthday_lunar:農曆生日,含 month/day/is_leap_monthbirthday_lunar_solar_this_year:農曆生日在當年對應的新曆日期(每年需更新)anniversaries:紀念日列表,每個紀念日有獨立 idnotes:備註created_at / updated_at:時間戳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 | anniversaryadvance_days:提前幾天提醒(默認 3 天)scheduled_task_id:對應 OpenClaw scheduled-task 的 IDenabled:是否啟用農曆轉新曆每年都會變,因此需要每年更新農曆生日對應的新曆日期。
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
pythondef calculate_reminder_date(target_date: date, advance_days: int) -> date: """計算提前提醒的日期,正確處理跨月情況""" return target_date - timedelta(days=advance_days)
pythondef 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
當用戶要求添加成員時:
pythonimport 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
pythondef 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(members, f, ensure_ascii=False, indent=2) return m return None
刪除成員時必須同步清理提醒:
pythondef delete_member(base_dir, member_id): """刪除成員及其所有提醒""" # 刪除成員 members_path = os.path.join(base_dir, "members.json") with open(members_path) as f: members = json.load(f) member = None new_members = [] for m in members: if m["id"] == member_id: member = m else: new_members.append(m) if not member: return None, [] with open(members_path, "w") as f: json.dump(new_members, f, ensure_ascii=False, indent=2) # 清理該成員的所有提醒 reminders_path = os.path.join(base_dir, "reminders.json") with open(reminders_path) as f: reminders = json.load(f) removed_task_ids = [] new_reminders = [] for r in reminders: if r["member_id"] == member_id: removed_task_ids.append(r.get("scheduled_task_id")) else: new_reminders.append(r) with open(reminders_path, "w") as f: json.dump(new_reminders, f, ensure_ascii=False, indent=2) return member, removed_task_ids
重要:刪除成員後,需要用 mcp__scheduled-tasks__update_scheduled_task 停用對應的 scheduled tasks(將 removed_task_ids 中的 task 設為 disabled)。
pythondef add_anniversary(base_dir, member_id, label, date_solar, recurring=True): """為成員添加紀念日""" 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: anniversary = { "id": str(uuid.uuid4()), "label": label, "date_solar": date_solar, "recurring": recurring } m["anniversaries"].append(anniversary) m["updated_at"] = datetime.utcnow().isoformat() + "Z" with open(path, "w") as f: json.dump(members, f, ensure_ascii=False, indent=2) return anniversary return None
pythondef remove_anniversary(base_dir, member_id, anniversary_id): """刪除成員的某個紀念日""" 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: m["anniversaries"] = [ a for a in m["anniversaries"] if a["id"] != anniversary_id ] m["updated_at"] = datetime.utcnow().isoformat() + "Z" with open(path, "w") as f: json.dump(members, f, ensure_ascii=False, indent=2) return True return False
pythonfrom datetime import date, timedelta import calendar def _safe_replace_year(d: date, year: int) -> date: """安全替換年份,處理 2/29 在非閏年的情況(fallback 到 2/28)""" if d.month == 2 and d.day == 29 and not calendar.isleap(year): return date(year, 2, 28) return d.replace(year=year) def upcoming_events(base_dir, days_ahead=30): """查詢未來 N 天內的生日和紀念日""" path = os.path.join(base_dir, "members.json") with open(path) as f: members = json.load(f) today = date.today() end_date = today + timedelta(days=days_ahead) events = [] for m in members: # 新曆生日 if m.get("birthday_solar"): bday = date.fromisoformat(m["birthday_solar"]) this_year = _safe_replace_year(bday, today.year) # 處理跨年情況 if this_year < today: this_year = _safe_replace_year(bday, today.year + 1) if today <= this_year <= end_date: age = this_year.year - bday.year events.append({ "date": str(this_year), "name": m["name"], "group": m["group"], "type": "新曆生日", "label": f"{m['name']} 的生日({age} 歲)", "member_id": m["id"] }) # 農曆生日(使用預計算的當年新曆日期) if m.get("birthday_lunar_solar_this_year"): lunar_solar = date.fromisoformat(m["birthday_lunar_solar_this_year"]) if today <= lunar_solar <= end_date: events.append({ "date": str(lunar_solar), "name": m["name"], "group": m["group"], "type": "農曆生日", "label": f"{m['name']} 的農曆生日(農曆{m['birthday_lunar']['month']}月{m['birthday_lunar']['day']}日)", "member_id": m["id"] }) # 紀念日 for ann in m.get("anniversaries", []): if ann.get("recurring") and ann.get("date_solar"): aday = date.fromisoformat(ann["date_solar"]) this_year = _safe_replace_year(aday, today.year) if this_year < today: this_year = _safe_replace_year(aday, today.year + 1) if today <= this_year <= end_date: years = this_year.year - aday.year events.append({ "date": str(this_year), "name": m["name"], "group": m["group"], "type": "紀念日", "label": f"{m['name']} — {ann['label']}(第 {years} 年)", "member_id": m["id"] }) return sorted(events, key=lambda e: e["date"])
pythondef search_members(base_dir, query=None, group=None): """搜索成員。支持按名字模糊搜索和按組別篩選。""" path = os.path.join(base_dir, "members.json") with open(path) as f: members = json.load(f) results = members if group: results = [m for m in results if m["group"] == group] if query: query_lower = query.lower() results = [m for m in results if query_lower in m["name"].lower() or query_lower in m.get("relationship", "").lower() or query_lower in m.get("notes", "").lower()] return results def list_groups(base_dir): """列出所有組別及成員數量""" path = os.path.join(base_dir, "members.json") with open(path) as f: members = json.load(f) groups = {} for m in members: g = m["group"] groups[g] = groups.get(g, 0) + 1 return groups
提醒使用 OpenClaw 的 mcp__scheduled-tasks__create_scheduled_task 工具來創建持久化的定時任務。
為成員設置生日提醒時,需要計算提醒日期並創建 scheduled task:
步驟:
1. 確定生日日期(新曆或農曆轉新曆)
2. 計算提醒日期 = 生日日期 - advance_days
3. 使用 mcp__scheduled-tasks__create_scheduled_task 創建任務
4. 將提醒信息寫入 reminders.json新曆生日提醒(每年固定日期):
taskId: "member-birthday-{member_name}-solar"
cronExpression: "0 9 {remind_day} {remind_month} *"
prompt: "🎂 提醒:{member_name} 的生日在 {advance_days} 天後({birthday_month}/{birthday_day})!請準備祝福!\n\n成員資料:\n- 關係:{relationship}\n- 組別:{group}\n- 備註:{notes}"
description: "{member_name} 生日提醒(提前 {advance_days} 天)"農曆生日提醒(日期每年變化,需配合年度更新):
taskId: "member-birthday-{member_name}-lunar"
cronExpression: "0 9 {remind_day} {remind_month} *"
prompt: "🎂 提醒:{member_name} 的農曆生日在 {advance_days} 天後(農曆{lunar_month}月{lunar_day}日,今年新曆{solar_date})!\n\n成員資料:\n- 關係:{relationship}\n- 組別:{group}\n- 備註:{notes}"
description: "{member_name} 農曆生日提醒(提前 {advance_days} 天)"紀念日提醒:
taskId: "member-anniversary-{member_name}-{anniversary_label}"
cronExpression: "0 9 {remind_day} {remind_month} *"
prompt: "💝 提醒:{member_name} 的{anniversary_label}在 {advance_days} 天後({date})!\n\n成員資料:\n- 關係:{relationship}\n- 備註:{notes}"
description: "{member_name} {anniversary_label}提醒"重要:不能簡單用 day - advance_days,必須用日期運算處理跨月情況:
pythonfrom datetime import date, timedelta import calendar def get_reminder_cron(target_month, target_day, advance_days): """計算提醒的 cron expression,正確處理跨月和跨年""" today = date.today() year = today.year m, d = int(target_month), int(target_day) # 處理 2/29 非閏年 fallback if m == 2 and d == 29 and not calendar.isleap(year): d = 28 target = date(year, m, d) # 如果今年的目標日期已過,使用明年 if target < today: year += 1 if m == 2 and int(target_day) == 29 and not calendar.isleap(year): d = 28 target = date(year, m, d) remind_date = target - timedelta(days=advance_days) # 返回 cron: 分 時 日 月 星期 return f"0 9 {remind_date.day} {remind_date.month} *"
pythondef save_reminder(base_dir, member_id, reminder_type, label, advance_days, task_id): """保存提醒記錄""" path = os.path.join(base_dir, "reminders.json") with open(path) as f: reminders = json.load(f) reminder = { "id": str(uuid.uuid4()), "member_id": member_id, "type": reminder_type, "label": label, "advance_days": advance_days, "scheduled_task_id": task_id, "enabled": True } reminders.append(reminder) with open(path, "w") as f: json.dump(reminders, f, ensure_ascii=False, indent=2) return reminder
應設置一個每年 1 月 1 日執行的 scheduled task,自動更新所有農曆生日的新曆日期:
taskId: "member-lunar-annual-update"
cronExpression: "0 10 1 1 *"
prompt: "執行 member-manager 年度農曆更新:
1. 讀取 members.json 中所有有農曆生日的成員
2. 使用 lunardate 將每個農曆生日轉換為今年的新曆日期
3. 更新 birthday_lunar_solar_this_year 欄位
4. 更新所有農曆生日提醒的 cron expression(因為新曆日期變了)
5. 回報更新結果"
description: "每年自動更新農曆生日對應的新曆日期"📋 {group} 成員列表(共 N 人)
1. {name}({relationship})
🎂 生日:{birthday_solar}(新曆)/ 農曆{lunar_month}月{lunar_day}日
📞 {phone}
📝 {notes}
2. ...📅 未來 {days} 天的重要日子:
🎂 {date} — {name} 的生日({age} 歲)
💝 {date} — {name} — {anniversary_label}(第 N 年)
...
共 N 個事件。✅ 已添加成員:{name}({group} / {relationship})
生日:{birthday_info}
已設置提醒:提前 {advance_days} 天
✅ 已刪除成員:{name}
已清理 {N} 個相關提醒
✅ 已更新 {N} 位成員的農曆生日新曆日期以下是用戶可以對 AI 說的話,以及 skill 應如何回應:
| 用戶說 | AI 應執行 | |--------|-----------| | 幫我加一個成員:媽媽,農曆三月初五生日 | add_member(group 默認「家庭」) + lunar_to_solar + 存檔 + 詢問是否設置提醒 | | 加同事小王,5月3號生日 | add_member(group「同事」)+ 存檔 | | 查看這個月有哪些生日 | upcoming_events(days_ahead=30),以表格顯示 | | 幫我提前 3 天提醒張三的生日 | 計算提醒日期 + create_scheduled_task + save_reminder | | 更新今年所有農曆生日的新曆日期 | update_all_lunar_birthdays + 更新相關提醒的 cron | | 顯示家庭組的所有成員 | search_members(group="家庭"),以列表顯示 | | 幫張三加一個結婚紀念日 6/20 | add_anniversary + 詢問是否設置提醒 | | 刪除成員李四 | 確認後 delete_member + 清理提醒 + 停用 scheduled tasks | | 修改張三的電話 | update_member(phone=...) | | 列出所有組別 | list_groups 顯示組別及人數 | | 查看所有提醒 | 讀取 reminders.json 並用 list_scheduled_tasks 顯示狀態 | | 設置每年自動更新農曆 | 創建 member-lunar-annual-update scheduled task |
用戶:幫我加一個成員,我媽媽,農曆三月初五生日
AI:
1. 解析信息:姓名=媽媽,關係=母親,農曆生日=三月初五
2. 計算今年新曆日期
3. 確認:
「確認添加以下成員:
- 姓名:媽媽
- 組別:家庭
- 關係:母親
- 農曆生日:三月初五(今年新曆 {date})
是否正確?要設置生日提醒嗎?」
4. 用戶確認後存檔
5. 如果要提醒,詢問提前幾天(默認 3 天)
6. 創建 scheduled task + 保存 reminder用戶可能用多種方式表達農曆日期:
本 skill 存儲個人可識別信息(PII),包括姓名、電話、生日等。所有數據以明文 JSON 存儲在本地磁碟。
必要的安全措施:
chmod 700 ~/.openclaw/workspace/users/)gpg 或加密壓縮包)python# 初始化時設置目錄權限 import stat os.makedirs(base_dir, exist_ok=True) os.chmod(base_dir, stat.S_IRWXU) # 700: 僅 owner 可讀寫執行
Session key 必須經過清洗才能用於構建文件路徑,防止目錄遍歷攻擊(如 ../../etc/passwd)。參見上方 sanitize_session_key() 函數。
當此 skill 被 AI agent 自主調用時:
推薦使用 lunardate 套件(PyPI: lunardate):
pip-audit 掃描確認birthday_lunar.is_leap_month 標記是否為閏月,轉換時需傳入 is_leap=True。若該年無閏月,自動 fallback 到非閏月lunardate 已安裝(pip install lunardate),並用 pip-audit 掃描_safe_replace_year() 自動 fallback 到 2/28(非閏年)Other measured skills in the registry, with their headline benchmark lift.