记录使用 Nextcloud AIO 搭建自托管日历服务,并通过 CalDAV、CLI 工具和本地 Agent 实现跨平台日历同步与安全写入的部署思路。

Nextcloud AIO + CalDAV:自建日历与本地 Agent 接入记录
12 mins
2340 words
Loading views

一、目标h2

这次折腾的目标很明确:不用第三方日历服务,搭一套自己可控的日历系统,并让本地 Agent 能够像普通客户端一样安全地读写日历。

最终希望做到:

  • 手机、桌面邮件客户端、网页端共用同一套日历。
  • 服务端使用 Nextcloud Calendar 作为 CalDAV 后端。
  • 本地 Agent 不直接进入容器或数据库,只通过 HTTPS CalDAV 操作日历。
  • 所有写入操作都必须先 dry-run,再经用户确认,最后写入并回读验证。
  • 应用密码、.env、UID、ETag、备份密码等敏感信息不进入日志和 Git。

最终链路如下:

手机 / 桌面日历客户端
│ CalDAV / HTTPS
Nextcloud Calendar
│ CalDAV / HTTPS
calendarctl CLI
本地 Agent / Skill

二、整体架构h2

服务端使用 Nextcloud AIO,前面放一层宿主机 Nginx 负责 HTTPS 和反向代理。

Cloudflare DNS
calendar.example.com
宿主机 Nginx :443
Nextcloud AIO Apache :11000
Nextcloud / PostgreSQL / Redis / Notify Push

AIO 管理页面只监听本机地址,不直接暴露到公网:

127.0.0.1:8080

需要访问 AIO 管理页面时,通过 SSH 隧道转发:

Terminal window
ssh -N -L 8080:127.0.0.1:8080 my-server

然后在本机浏览器打开:

https://127.0.0.1:8080

这样 AIO 管理入口不会暴露在公网,只在需要维护时临时访问。

三、Nextcloud AIO 部署要点h2

AIO 主容器只需要暴露本机管理端口,Apache 服务则绑定到宿主机回环地址,由 Nginx 转发。

关键配置大致如下:

127.0.0.1:8080:8080
APACHE_PORT=11000
APACHE_IP_BINDING=127.0.0.1

核心容器包括:

nextcloud-aio-mastercontainer
nextcloud-aio-apache
nextcloud-aio-nextcloud
nextcloud-aio-database
nextcloud-aio-redis
nextcloud-aio-notify-push

这次没有启用 Office、Talk、Imaginary、Whiteboard、ClamAV 等组件。对单人或轻量使用场景来说,少开组件可以减少资源占用和维护复杂度。

四、Nginx 与 HTTPSh2

宿主机 Nginx 负责接收公网 80/443 请求,Nextcloud AIO Apache 只监听本机:

127.0.0.1:11000

配置完成后先检查 Nginx:

Terminal window
nginx -t

证书可以使用 Certbot 签发,并确认自动续期定时器正常:

Terminal window
systemctl is-enabled certbot.timer
systemctl is-active certbot.timer
certbot renew --dry-run

如果 renew --dry-run 能成功,说明证书续期链路基本可用。

五、账户与日历设计h2

不要让 Agent 使用管理员账户。比较稳妥的方式是创建一个专用普通用户,例如:

calendar-bot

再由管理员创建一个正式日历,例如:

工作日历

然后共享给 calendar-bot,授予可编辑权限。

在 CalDAV 中需要特别注意一点:日历显示名称可以改,但 CalDAV 路径不要随意改。客户端或自动化工具应该固定使用发现到的日历路径,而不是依赖可变的显示名。

示例路径:

/remote.php/dav/calendars/calendar-bot/work_shared_by_admin/

关键原则:

显示名称可以改
CalDAV 路径不要改
自动化工具固定使用 calendar-path

六、CalDAV 联通验证h2

CalDAV 基础地址通常是:

https://calendar.example.com/remote.php/dav/

认证成功后,DAV PROPFIND 请求应返回:

HTTP 207 Multi-Status

日历发现结果可以整理成类似结构:

{
"name": "工作日历 (admin)",
"displayname": "工作日历 (admin)",
"path": "/remote.php/dav/calendars/calendar-bot/work_shared_by_admin/",
"color": "#5b64b3"
}

只要能稳定发现日历路径,后续 CLI 和 Agent 就可以围绕这个路径工作。

七、calendarctl CLI 设计h2

我没有让 Agent 直接拼 CalDAV 请求,而是封装了一个本地 CLI 工具 calendarctl

项目结构大致如下:

~/Dev/calendar-agent/
├── bin/calendarctl
├── calendarctl/
├── skills/
├── tests/
├── pyproject.toml
└── .env

CLI 入口只负责调用项目虚拟环境中的 Python:

Terminal window
exec "$PROJECT_DIR/.venv/bin/python" -m calendarctl.cli "$@"

.env 保存运行时配置:

NEXTCLOUD_URL
NEXTCLOUD_USERNAME
NEXTCLOUD_APP_PASSWORD
NEXTCLOUD_CALENDAR_NAME
NEXTCLOUD_TIMEZONE

权限应设置为:

Terminal window
chmod 600 .env

并确认没有被 Git 跟踪:

Terminal window
git ls-files -- .env

无输出才是预期结果。

八、CLI 功能与安全流程h2

8.1 日历发现h3

Terminal window
./bin/calendarctl calendars --json

用于确认当前账户可访问哪些日历,以及每个日历对应的 CalDAV 路径。

8.2 查询事件h3

查询时建议显式传入时间范围:

Terminal window
./bin/calendarctl list \
--from "2026-07-29 00:00" \
--to "2026-07-29 23:59" \
--calendar-path "/remote.php/dav/calendars/calendar-bot/work_shared_by_admin/" \
--json

这样可以避免一次性拉取过多历史事件,也便于 Agent 控制上下文大小。

8.3 新建事件h3

新建事件必须走固定流程:

解析自然语言
→ dry-run
→ 展示拟写入内容
→ 用户确认
→ --yes 写入
→ list 回读验证

写入时使用:

If-None-Match: *

可以避免 UID 冲突时覆盖已有事件。

8.4 更新事件h3

更新事件需要先读取当前 ETag,再带 If-Match 写入:

按 UID 定位
→ 获取当前 ETag
→ dry-run
→ 用户确认
→ If-Match 写入
→ 回读验证

如果事件在操作过程中被其他客户端修改,ETag 发生变化,服务端会通过 HTTP 412 拒绝覆盖。

8.5 删除与恢复h3

删除同样需要基于 ETag:

按 UID 定位
→ 获取 ETag
→ dry-run
→ 用户确认
→ If-Match 删除

恢复删除事件时,优先使用 Nextcloud 原生 CalDAV 回收站,而不是重新 PUT 一个相同 UID 的事件。

示例:

MOVE + If-Match

这样能保留服务端对删除和恢复的语义,也减少 UID 冲突风险。

九、Agent Skill 约束h2

本地 Agent 只允许通过 CLI 操作日历,不直接读取 .env,也不直接发 CalDAV 请求。

Skill 中应明确禁止:

  • 直接发 CalDAV 请求。
  • 直接读取 .env
  • 输出应用密码。
  • 绕过 dry-run。
  • 未经确认写入。
  • 修改固定的 calendar-path

标准流程固定为:

解析
→ dry-run
→ 展示变更
→ 用户确认
→ --yes
→ 读取回验

这个约束非常重要。Agent 可以帮忙操作,但不应该拥有“静默修改日历”的能力。

十、Python 打包问题h2

项目里同时存在 CLI 代码和 Skill 文档时,Setuptools 可能误识别多个顶层包,例如:

Multiple top-level packages discovered in a flat-layout

可以在 pyproject.toml 中明确包发现范围:

[tool.setuptools.packages.find]
where = ["."]
include = ["calendarctl*"]
exclude = ["skills*", "tests*", "docs*"]
namespaces = false

这样只打包 CLI 代码,不把 Skill、测试和文档打进 Python 包。

测试使用:

Terminal window
.venv/bin/python -m pytest tests/ -q

十一、手机与桌面客户端接入h2

11.1 iPhoneh3

在 Nextcloud 中为手机创建独立应用密码:

头像
→ 个人设置
→ 安全
→ 设备和会话
→ 创建应用密码

然后在 iPhone 中添加 CalDAV:

设置
→ App
→ 日历
→ 日历账户
→ 添加账户
→ 其他
→ 添加 CalDAV 账户

填写示例:

服务器:calendar.example.com
用户名:nextcloud-admin
密码:iPhone 专用应用密码
描述:Nextcloud

建议每台设备使用独立应用密码,设备停用后可以单独撤销。

11.2 Thunderbirdh3

Thunderbird 原生支持 CalDAV。

路径:

Thunderbird
→ 日历
→ 新建日历
→ 网络
→ CalDAV

推荐填写:

用户名:nextcloud-admin
位置:https://calendar.example.com/remote.php/dav/

认证同样使用 Thunderbird 专用应用密码。

如果自动发现失败,可以尝试更具体的用户日历路径:

https://calendar.example.com/remote.php/dav/calendars/nextcloud-admin/

十二、备份与自启动h2

Nextcloud AIO 支持 Borg 备份。备份目录可以放在宿主机,例如:

/srv/nextcloud-backup

首次备份后应确认:

Last backup successful

也可以检查 Borg 仓库大小:

Terminal window
du -sh /srv/nextcloud-backup/borg

自动备份时间需要注意服务器时区。例如希望北京时间每天 03<30> 备份,而服务器使用 UTC,则应配置为:

19:30 UTC

基础服务自启动也要确认:

Terminal window
systemctl is-enabled docker
systemctl is-enabled nginx
systemctl is-active docker
systemctl is-active nginx

Nextcloud AIO 的 Borg 备份容器不需要常驻运行,它只在备份期间启动,这是正常现象。

十三、安全注意事项h2

不要公开或提交:

Nextcloud 应用密码
Borg 备份密码
.env
AIO 管理密码
真实 UID / ETag
真实服务器路径
真实内网拓扑

建议:

  • 每台设备单独生成应用密码。
  • 设备停用后单独撤销应用密码。
  • Borg 密码保存到密码管理器。
  • .env 权限保持 600
  • 定期确认备份成功时间。
  • Agent 写入操作永远需要用户确认。

十四、排障经验h2

这次部署中最容易踩坑的点有几个:

  • AIO 管理端口不要暴露到公网。
  • Nextcloud 域名、反向代理和 AIO Apache 端口要对应一致。
  • CalDAV 显示名称可以改,但路径不应随意改。
  • Agent 不应直接操作容器、数据库或 .env
  • 更新和删除事件必须基于 ETag,避免覆盖其他客户端的改动。
  • 手机和桌面客户端最好使用独立应用密码。
  • 备份密码必须保存好,Borg 密码丢失后无法恢复备份。

最终比较稳定的形态是:Nextcloud 负责标准 CalDAV,手机和桌面客户端作为普通日历客户端接入,本地 Agent 只通过 CLI 和 CalDAV 做受控读写。

十五、常用命令速查h2

发现日历:

Terminal window
cd ~/Dev/calendar-agent
./bin/calendarctl calendars --json

查询指定日期:

Terminal window
./bin/calendarctl list \
--from "2026-07-29 00:00" \
--to "2026-07-29 23:59" \
--calendar-path "/remote.php/dav/calendars/calendar-bot/work_shared_by_admin/" \
--json

运行测试:

Terminal window
.venv/bin/python -m pytest tests/ -q

检查 Git:

Terminal window
git status --short
git ls-files -- .env

检查 AIO 容器:

Terminal window
docker ps --format 'table {{.Names}}\t{{.Status}}' | grep nextcloud-aio

检查备份:

Terminal window
du -sh /srv/nextcloud-backup/borg

检查证书续期:

Terminal window
systemctl is-enabled certbot.timer
systemctl is-active certbot.timer
certbot renew --dry-run

十六、最终效果h2

完成后,整套链路可以形成闭环:

Nextcloud AIO
→ HTTPS 反向代理
→ CalDAV 账户与共享日历
→ calendarctl CLI
→ 本地 Agent / Skill
→ 手机和桌面客户端双向同步
→ Borg 自动备份
→ Certbot 自动续期

这套方案的重点不在于“让 Agent 能操作日历”,而在于让它以普通客户端身份、在明确边界内、安全地操作日历。