[教程] 无需第三方 App,让 AI Agent 实现 iOS 原生通知提醒 | 全平台通用,保姆级教程

mochii 2026-09-08 23:02 1


给 AI Agent 接入 iOS 消息推送,通常需要依赖 Server酱、Bark 等第三方服务。


但其实苹果自带的 iCloud 日历就是天然且免费的通知通道,只需要一个简单的 HTTP/Python 脚本,就能向用户推送原生日历弹窗。


本教程演示如何利用 Apple CalDAV 开放接口,为 AI Agent 配置 iCloud 日历准点提醒技能,可将本文链接直接发给 AI 部署



兼容性:适用于所有 AI Agent(如 Hermes、OpenClaw、PI 等)。

环境验证:Debian 13 + Python 3.13 验证通过。



前置条件



  1. Apple ID 已开启双重认证(2FA)

  2. 能直连 caldav.icloud.com:443


第一步:生成 App 专用密码



  1. 浏览器打开 https://account.apple.com/account/manage/section/security

  2. 「登录与安全性」→ App 专用密码 → 生成

  3. 保存 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>

对照 hrefdisplayname 判断日历路径,把第五步各版本 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 里不显示),所以本方案被迫采用日历事件。

最新回复 (6)
  • Tansuo 09-08 23:04
    1

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

  • mochii 楼主 09-08 23:08
    2

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

  • 飞飞虎 09-08 23:22
    3

    太^-^了啊大佬

  • junhua 09-08 23:37
    4

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

  • 灰鸽 09-08 23:39
    5

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

  • Eevee 09-08 23:42
    6

    我用的是喵提醒


    效果差不多

* 帖子来源Linux.do
返回