
给 AI Agent 接入 iOS 消息推送,通常需要依赖 Server酱、Bark 等第三方服务。
但其实苹果自带的 iCloud 日历就是天然且免费的通知通道,只需要一个简单的 HTTP/Python 脚本,就能向用户推送原生日历弹窗。
本教程演示如何利用 Apple CalDAV 开放接口,为 AI Agent 配置 iCloud 日历准点提醒技能,可将本文链接直接发给 AI 部署。
兼容性:适用于所有 AI Agent(如 Hermes、OpenClaw、PI 等)。
环境验证:Debian 13 + Python 3.13 验证通过。
前置条件
- Apple ID 已开启双重认证(2FA)
- 能直连
caldav.icloud.com:443
第一步:生成 App 专用密码
- 浏览器打开 https://account.apple.com/account/manage/section/security
- 「登录与安全性」→ App 专用密码 → 生成
- 保存
xxxx-xxxx-xxxx-xxxx 格式密码(只显示一次)

第二步:确认 Apple ID
点击电子邮件和电话号码,主要电话号码或主要电子邮件地址就是 Apple ID,用于接下来的 CalDAV 认证。

第三步:连通性验证
向 iCloud 日历列表发送 PROPFIND 请求,预期输出格式示例 /12112653923/principal/,需记录此数字 ID 给第五步用。
须填写:
- 用户名 = 第二步的 Apple ID
- 密码 = 第一步的 App 专用密码
Linux / macOS
curl -s -u "用户名:密码" -X PROPFIND -H "Depth: 0" "https://caldav.icloud.com/.well-known/caldav" | grep -o '/[0-9]*/principal/'
Windows (PowerShell)
curl.exe -s -u "用户名:密码" -X PROPFIND -H "Depth: 0" "https://caldav.icloud.com/.well-known/caldav" | Select-String -Pattern '/[0-9]*/principal/' | ForEach-Object { $_.Matches.Value }
第四步:自定义日历(可选)
推送通知默认写入「个人」日历(路径 calendars/home/),可以修改为写入其他日历,这样就能单独设置颜色和归属,以便区分是否由 AI 写入。
一、获取日历路径
以下是获取所有日历路径的指令,须填写:
- 用户名 = 第二步的 Apple ID
- 密码 = 第一步的 App 专用密码
Linux / macOS
curl -s -u "用户名:密码" -X PROPFIND -H "Depth: 1" \
-H "Content-Type: application/xml" \
--data-binary '<?xml version="1.0"?><d:propfind xmlns:d="DAV:"><d:prop><d:displayname/></d:prop></d:propfind>' \
"https://caldav.icloud.com/{PRINCIPAL_ID}/calendars/" \
| grep -oE '<href>[^<]+</href>|<displayname[^>]*>[^<]+</displayname>'
Windows (PowerShell)
$OutputEncoding = [Console]::OutputEncoding = [Text.Encoding]::UTF8
curl.exe -sL -u "用户名:密码" -X PROPFIND -H "Depth: 1" -H "Content-Type: application/xml; charset=utf-8" --data-binary '<?xml version="1.0"?><d:propfind xmlns:d="DAV:"><d:prop><d:displayname/></d:prop></d:propfind>' "https://caldav.icloud.com/{PRINCIPAL_ID}/calendars/" | Select-String -Pattern '<([^:]+:)?(href|displayname)[^>]*>[^<]+</([^:]+:)?(href|displayname)>' -AllMatches | ForEach-Object { $_.Matches.Value }
二、查看日历路径
查询结果示例:
<href>/12112653923/calendars/</href>
<displayname xmlns="DAV:">明子</displayname>
<href>/12112653923/calendars/478D5D08-0F22-4B80-A10B-615BCFBE9135/</href>
<displayname xmlns="DAV:">飞行计划</displayname>
<href>/12112653923/calendars/6ba4d928-4c4f-412c-8c52-4689177c1975/</href>
<displayname xmlns="DAV:">Reminders ⚠️</displayname>
<href>/12112653923/calendars/home/</href>
<displayname xmlns="DAV:">个人</displayname>
<href>/12112653923/calendars/inbox/</href>
<href>/12112653923/calendars/notification/</href>
<href>/12112653923/calendars/work/</href>
<displayname xmlns="DAV:">工作</displayname>
<href>/12112653923/calendars/outbox/</href>
对照 href 与 displayname 判断日历路径,把第五步各版本 URL 里的 calendars/home/ 换掉即可(如替换为 calendars/work/)。
需忽略无 displayname 的系统目录:inbox/、notification/、outbox/。
第五步:配置脚本
以下四种实现任选其一。
替换三处:
- 用户名:Apple ID
- 密码:App 专用密码
- {PRINCIPAL_ID}:第三步连通性验证输出里 principal URL 的数字 ID,如 12112653923
Python(推荐 AI Agent 使用,便于程序内生成时间戳)
把以下代码存为脚本,如 ~/scripts/add_reminder.py
import base64, urllib.request, uuid
from datetime import datetime, timedelta, timezone
USER, PASS = "8612345678900", "XXXX-XXXX-XXXX-XXXX"
PRINCIPAL_ID = "12112653923"
cred = base64.b64encode(f"{USER}:{PASS}".encode()).decode()
def add_reminder(summary: str, when: datetime, minutes: int = 30):
"""在'个人'日历建一条事件,when 时刻弹通知,持续 minutes 分钟"""
now = datetime.now(timezone.utc)
if when.tzinfo is None:
when = when.replace(tzinfo=timezone(timedelta(hours=8))) # 默认按北京时间
s, e = when.astimezone(timezone.utc), when.astimezone(timezone.utc) + timedelta(minutes=minutes)
fmt = lambda d: d.strftime("%Y%m%dT%H%M%SZ")
uid = f"hermes-{uuid.uuid4().hex[:12]}"
ics = (f"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Hermes//CN\r\n"
f"BEGIN:VEVENT\r\nUID:{uid}\r\nDTSTAMP:{fmt(now)}\r\n"
f"DTSTART:{fmt(s)}\r\nDTEND:{fmt(e)}\r\nSUMMARY:{summary}\r\n"
f"BEGIN:VALARM\r\nTRIGGER:-PT0M\r\nACTION:DISPLAY\r\n"
f"DESCRIPTION:提醒\r\nEND:VALARM\r\nEND:VEVENT\r\nEND:VCALENDAR")
url = f"https://caldav.icloud.com/{PRINCIPAL_ID}/calendars/home/{uid}.ics"
req = urllib.request.Request(url, data=ics.encode(), method="PUT",
headers={"Authorization": f"Basic {cred}",
"Content-Type": "text/calendar; charset=utf-8",
"If-None-Match": "*"})
urllib.request.urlopen(req, timeout=15)
return uid
if __name__ == "__main__":
import sys
if len(sys.argv) >= 3:
print("已写入:", add_reminder(sys.argv[1], datetime.strptime(sys.argv[2], "%Y-%m-%d %H:%M")))
else:
print('用法: python add_reminder.py "<标题>" "<YYYY-MM-DD HH:MM>"')
注意事项:
- 行尾必须 CRLF(
\r\n),纯 \n 部分客户端会拒绝
If-None-Match: "*" 防止覆盖同名事件
TRIGGER:-PT0M = 准点提醒;要提前 10 分钟就写 -PT10M
- 时间戳一律 UTC(
Z 后缀),时区转换在客户端做
curl(零依赖)
先把事件内容保存为 event.ics(注意 Windows 记事本保存时需选 CRLF),DTSTART:20260909T070000Z = 北京时间 15:00(UTC+8 换算):
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Hermes//CN
BEGIN:VEVENT
UID:my-reminder-001
DTSTAMP:20260909T050000Z
DTSTART:20260909T070000Z
DTEND:20260909T073000Z
SUMMARY:取快递
BEGIN:VALARM
TRIGGER:-PT0M
ACTION:DISPLAY
DESCRIPTION:提醒
END:VALARM
END:VEVENT
END:VCALENDAR
发送以下命令,返回 201 即成功,文件名(my-reminder-001.ics)是事件 UID,需保持唯一:
curl -u "用户名:密码" -X PUT \
-H "Content-Type: text/calendar; charset=utf-8" \
-H "If-None-Match: *" \
--data-binary @event.ics \
"https://caldav.icloud.com/{PRINCIPAL_ID}/calendars/home/my-reminder-001.ics"
Node.js
用法:addReminder(“取快递”, new Date(“2026-09-09T15:00:00+08:00”))
const USER = "用户名", PASS = "密码", PID = "{PRINCIPAL_ID}";
const auth = Buffer.from(`${USER}:${PASS}`).toString("base64");
const pad = n => String(n).padStart(2, "0");
const fmt = d => d.getUTCFullYear() + pad(d.getUTCMonth()+1) + pad(d.getUTCDate())
+ "T" + pad(d.getUTCHours()) + pad(d.getUTCMinutes()) + pad(d.getUTCSeconds()) + "Z";
async function addReminder(summary, when, minutes = 30) {
const now = new Date(), start = when, end = new Date(when.getTime() + minutes*60000);
const uid = "hermes-" + Math.random().toString(36).slice(2, 14);
const ics = ["BEGIN:VCALENDAR","VERSION:2.0","PRODID:-//Hermes//CN","BEGIN:VEVENT",
`UID:${uid}`, `DTSTAMP:${fmt(now)}`, `DTSTART:${fmt(start)}`, `DTEND:${fmt(end)}`,
`SUMMARY:${summary}`,"BEGIN:VALARM","TRIGGER:-PT0M","ACTION:DISPLAY",
"DESCRIPTION:提醒","END:VALARM","END:VEVENT","END:VCALENDAR"].join("\r\n");
const res = await fetch(`https://caldav.icloud.com/${PID}/calendars/home/${uid}.ics`, {
method: "PUT",
headers: { Authorization: `Basic ${auth}`, "Content-Type": "text/calendar; charset=utf-8", "If-None-Match": "*" },
body: ics });
if (res.status !== 201) throw new Error("HTTP " + res.status);
return uid;
}
Windows PowerShell
$pair = "用户名:密码"
$auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($pair))
$uid = "my-reminder-" + (Get-Random -Maximum 99999)
$PID_ = "PRINCIPAL_ID"
$nl = [char]13 + [char]10 # CRLF 行尾,必须
$ics = ("BEGIN:VCALENDAR","VERSION:2.0","PRODID:-//Hermes//CN",
"BEGIN:VEVENT","UID:$uid","DTSTAMP:20260909T050000Z",
"DTSTART:20260909T070000Z","DTEND:20260909T073000Z",
"SUMMARY:取快递","BEGIN:VALARM","TRIGGER:-PT0M","ACTION:DISPLAY",
"DESCRIPTION:提醒","END:VALARM","END:VEVENT","END:VCALENDAR") -join $nl
Invoke-WebRequest -Uri "https://caldav.icloud.com/$PID_/calendars/home/$uid.ics" `
-Method Put -Headers @{Authorization="Basic $auth"; "If-None-Match"="*"} `
-ContentType "text/calendar; charset=utf-8" -Body $ics -UseBasicParsing
# StatusCode 201 = 成功
第六步:AI 自动调用
在 AI Agent 技能目录里新建 SKILL.md,填入技能描述及第五步的脚本地址,以 Hermes 为例:
~/.hermes/skills/linux/apple-calendar/SKILL.md
---
name: apple-calendar-reminder
description: 当用户说"记个待办/提醒我 xxx(时间)"时使用。通过 CalDAV 写入 iCloud 日历,手机准点弹通知。
---
执行 `python ~/scripts/add_reminder.py "<标题>" "<YYYY-MM-DD HH:MM>"`
- 时间为北京时间 24 小时制;用户没给时间就设当天 21:00
- 执行成功回复"已记:xxx,将在 HH:MM 提醒"
附:踩坑记录
本方案的最佳实现应当是推送到「提醒事项」,但 iOS 13 之后提醒事项改用苹果私有 CloudKit 协议同步,CalDAV 只能访问遗留清单(写入后 App 里不显示),所以本方案被迫采用日历事件。