• mochii

    图片

    给 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/&quot; \
    | 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/&quot; | 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&quot;
    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&quot;
    ```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&quot; `
    -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 里不显示),所以本方案被迫采用日历事件。

    主楼
  • Tansuo

    佬友这个思路是真的无敌,太强了!

    #1
  • mochii

    感谢认可,写了一晚上 ^-^

    #2
  • 飞飞虎

    太^-^了啊大佬

    #3
  • junhua

    感觉很方便,一直想利用上ios的自带日历

    #4
  • 灰鸽

    大佬牛啊,我让 ai 跑下你的流程试试

    #5
  • Eevee

    我用的是喵提醒

    效果差不多

    #6

发表回复

登录后回复