我希望把 Hermes Agent 做成一个本地个人助理:通过 TG 或微信发一句自然语言,让它理解日期、时间、持续时长和提醒方式,检查日历冲突,然后写入苹果原生日历。
最终这套流程已经在本地跑通:
Telegram / 微信
↓
Hermes Gateway
↓
Hermes 理解自然语言并检查必要字段
↓
accli 查询日历、检查重复和时间冲突
↓
hermes-calendar-create 创建带提醒的日程
↓
Apple Calendar / iCloud 同步这篇文章记录完整配置,也重点整理过程中遇到的问题。很多问题单独看都不复杂,但终端、launchd、代理、macOS 隐私权限和日历提醒叠加在一起后,很容易误判故障位置。
一、我的运行环境
- 设备:MacBook Neo,8GB 内存、512GB 存储
- 系统:macOS
- Hermes Agent:v0.21.1,Git 安装
- Hermes 安装目录:
~/.hermes/hermes-agent - Hermes 工作区:
~/Hermes-Workspace - 模型:
gpt-5.6-sol - 模型提供方:OpenAI Codex 登录
- 消息入口:TG,后来也连接了微信
- 默认时区:
Asia/Shanghai - 默认日历:在 Apple 日历上创建的
Hermes 日程
需要注意,本文中的版本和命令对应我的实际环境。Hermes 和 accli 更新较快,安装时应先用 hermes --version、accli --version 和相应的 --help 检查当前版本。
二、先明确目标:不是“能创建”就算完成
我需要的不是让模型随便猜一个时间并写进日历,而是一套可控的日程助理逻辑。
创建普通日程前,Hermes 必须知道:
- 标题;
- 日期;
- 开始时间;
- 结束时间或持续时长;
- 提前多久提醒,或者明确不提醒。
地点和备注是可选项,用户没说就不用追问。如果缺少多个必要信息,应当一次问完,不能一次只问一个。信息完整、没有冲突时直接创建,不需要再次确认;修改和删除则必须先展示变化并征得确认。
例如:
帮我安排明天下午三点到四点半在图书馆复习高数,提前二十分钟提醒。
这句话已经包含全部必要信息,可以直接进入冲突检查和创建流程。
而下面这句话信息不完整:
明天下午提醒我复习高数。
Hermes 应一次性询问具体开始时间、持续时长以及提前多久提醒,不能自行假设。
三、第一个问题:Homebrew 和完整 Xcode
最初我尝试安装另一个 Apple Calendar CLI:
brew install sichengchen/tap/apple-calendar-cli一开始终端甚至没有 brew 命令。安装 Homebrew 后,又遇到了两个提示:
- 第三方 tap 尚未被 Homebrew 信任;
- 这个工具需要完整的 Xcode.app,只有 Command Line Tools 不够。
为了一个日历命令行工具下载完整 Xcode,成本太高,也不适合我的轻量配置。因此我放弃这条路线,改用基于 Node.js、JXA 和 EventKit 的 accli。
这一步的经验是:macOS 工具都能操作日历,但依赖成本差异很大。安装前先确认是否需要完整 Xcode、额外运行时或编译环境。
四、改用 accli
我的 Hermes 环境已经有 Node.js,因此把 accli 安装到用户目录:
npm install --global --prefix "$HOME/.local" @joargp/accli确保 ~/.local/bin 在 PATH 中:
export PATH="$HOME/.local/bin:$PATH"如果希望每次打开终端都生效,可以把这一行放进 ~/.zshrc,然后执行:
source ~/.zshrc完成安装后先初始化权限,再列出日历:
accli setup
accli calendars --json我在苹果日历中单独创建了一个名为“Hermes 日程”的 iCloud 日历,然后将其设为默认日历:
accli config set-default --calendar "Hermes 日程"
accli config show单独建立一个 Hermes 日历的好处是便于识别、筛选和撤销,也避免测试阶段把事件混进学习、家庭或工作日历。
五、第二个问题:当前 accli 没有 --alert
最初设计的创建命令是:
accli create "Hermes 日程" \
--summary "复习高数" \
--start "2026-09-14T15:00" \
--end "2026-09-14T16:30" \
--alert 20 \
--json但我当时安装的 accli 版本并不支持 --alert。如果直接让 Hermes 使用这条命令,它要么报参数错误,要么创建一个没有提醒的事件。
我的解决方案是把工作拆开:
accli负责读取日历、检查完全重复和时间冲突;- 自定义的
hermes-calendar-create负责创建事件并添加提醒。
创建 JXA 脚本
脚本位置:
~/.local/share/hermes-calendar/create-event.js内容如下:
function run(argv) {
if (argv.length < 4) {
throw new Error(
'Usage: title start end alerts [location] [notes]\n' +
'alerts: positive minutes such as 15 or 15,1440; use none for no alert'
);
}
var calendarName = "Hermes 日程";
var title = argv[0];
var startText = argv[1];
var endText = argv[2];
var alertsText = argv[3];
var locationText = argv.length > 4 ? argv[4] : "";
var notesText = argv.length > 5 ? argv[5] : "";
var startDate = new Date(startText);
var endDate = new Date(endText);
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
throw new Error("Invalid date. Use YYYY-MM-DDTHH:mm");
}
if (endDate.getTime() <= startDate.getTime()) {
throw new Error("End time must be later than start time");
}
var Calendar = Application("Calendar");
var matches = Calendar.calendars.whose({name: calendarName})();
if (matches.length !== 1) {
throw new Error(
'Expected exactly one calendar named "' +
calendarName +
'", found ' +
matches.length
);
}
var properties = {
summary: title,
startDate: startDate,
endDate: endDate
};
if (locationText) {
properties.location = locationText;
}
if (notesText) {
properties.description = notesText;
}
var calendar = matches[0];
var event = Calendar.Event(properties);
calendar.events.push(event);
var alerts = [];
if (alertsText.toLowerCase() !== "none") {
var parts = alertsText.split(",");
for (var i = 0; i < parts.length; i++) {
var minutes = Number(parts[i].trim());
if (!isFinite(minutes) || minutes < 0) {
throw new Error("Alert minutes must be zero or a positive number");
}
event.displayAlarms.push(
Calendar.DisplayAlarm({
triggerInterval: -minutes
})
);
alerts.push(minutes);
}
}
Calendar.reloadCalendars();
var eventID = "";
try {
eventID = event.uid();
} catch (error) {}
return JSON.stringify({
ok: true,
eventId: eventID,
calendar: calendarName,
title: title,
start: startText,
end: endText,
alertsMinutesBefore: alerts,
location: locationText
});
}创建命令包装器
包装器位置:
~/.local/bin/hermes-calendar-create内容如下:
#!/bin/zsh
set -eu
exec /usr/bin/osascript \
-l JavaScript \
"$HOME/.local/share/hermes-calendar/create-event.js" \
"$@"给它执行权限:
chmod +x ~/.local/bin/hermes-calendar-create调用格式:
hermes-calendar-create \
"标题" \
"YYYY-MM-DDTHH:mm" \
"YYYY-MM-DDTHH:mm" \
"提醒分钟" \
"地点" \
"备注"多个提醒使用英文逗号分隔,例如 30,1440;不提醒则传入 none。脚本返回 JSON,只有出现 "ok": true 才能认定创建成功。
六、第三个问题:Hermes 必须知道什么时候追问
工具能够创建事件后,还需要约束 Agent 的决策。否则模型可能擅自补全持续时长、误解“下午”,或者没有检查冲突就重复创建。
我在 ~/Hermes-Workspace/.hermes.md 中加入了 Apple Calendar 规则,核心内容如下:
## Apple Calendar 一句话创建规则
- 默认时区:Asia/Shanghai。
- 默认日历:Hermes 日程。
- 使用 accli 查询事件、检查重复和时间冲突。
- 使用 hermes-calendar-create 创建带提醒的普通日程。
- 不要使用 accli create,因为当前安装版本无法添加提醒。
创建前必须确定:
1. 标题;
2. 日期;
3. 开始时间;
4. 结束时间或持续时长;
5. 提醒提前量,或者用户明确表示不提醒。
地点和备注是可选字段。缺少多个必要字段时,在一条消息中一次性问完,不得自行猜测持续时长和提醒时间。
用户补充信息后,与上一条待创建请求合并。所有必要信息完整后检查重复与冲突;没有冲突则直接创建,不进行第二次确认。
“下午”“晚上”“一会儿”等不精确时间必须追问。“到时候提醒我”没有说明提前量,也必须追问。相对日期需要按照 Asia/Shanghai 的当前日期转换为明确日期。
只有在用户明确要求安排、创建、添加或记到日历时才能写入。普通讨论不能触发创建。
创建命令:
hermes-calendar-create "标题" "开始时间" "结束时间" "提醒分钟" "地点" "备注"
修改和删除日程前,必须展示目标事件和变化并获得确认。.hermes.md 可以写得比普通提示词长一些,但应只保留最新规则,删除互相冲突的旧版本。Hermes 对项目上下文文件有长度限制;长期规则越来越多时,更适合拆成独立 Skill。
七、第四个问题:前台能运行,后台却连不上模型和 Telegram
Hermes Gateway 前台运行正常后,我把它安装成了 macOS 的 launchd 后台服务:
hermes gateway install
hermes gateway start
hermes gateway status随后通过 Telegram 创建日程时,日志连续出现:
APITimeoutError <- ConnectTimeout
stream_opened=false
base_url=https://chatgpt.com/backend-api/codexTelegram 也出现:
httpx.RemoteProtocolError: Server disconnected without sending a response这说明故障发生在模型调用阶段,日历工具根本还没有开始运行。由于模型和 Telegram 两个外网服务同时失败,原因指向后台 Gateway 没有正确走代理。
我之前只在终端的 hermes() shell 函数里配置代理。launchd 不会读取交互式终端函数,因此前台可用不代表后台可用。
解决办法是把实际代理配置写入 ~/.hermes/.env:
HTTP_PROXY=http://127.0.0.1:实际端口
HTTPS_PROXY=http://127.0.0.1:实际端口
ALL_PROXY=http://127.0.0.1:实际端口
TELEGRAM_PROXY=http://127.0.0.1:实际端口
NO_PROXY=localhost,127.0.0.1,::1这里必须使用代理软件真实的 HTTP 或 Mixed Port。不要照抄示例端口,也不要在公开文章中暴露 ~/.hermes/.env 里的机器人 Token 和认证信息。
Hermes 默认允许 Gateway 读取环境代理。为了明确,也可以在 ~/.hermes/config.yaml 中设置:
gateway:
trust_env: true保存后重启:
hermes gateway restart这一问题给我的最大提醒是:在 macOS 上,终端环境和 launchd 后台环境是两套环境。代理、PATH、工作目录和系统权限都要分别验证。
八、第五个问题:后台 Gateway 的工作目录不正确
我的日历规则位于:
~/Hermes-Workspace/.hermes.mdHermes 会从会话工作目录发现项目规则。如果 Gateway 使用安装目录或其他目录启动,新的会话可能读不到这份规则。
因此在 ~/.hermes/config.yaml 中设置:
terminal:
backend: local
cwd: /Users/burike/Hermes-Workspace
timeout: 180
home_mode: auto修改规则或工作目录后,需要重启 Gateway,并在 Telegram 中发送 /new 创建新 Session。项目规则是在会话开始时加载的,继续使用旧 Session 不一定会刷新规则。
另外,launchd 会在安装服务时记录 PATH。安装新的命令行工具后,如果后台找不到命令,可以重新运行:
hermes gateway install
hermes gateway restart九、第六个问题:NOT_AUTHORIZED,但设置里明明已经允许
网络恢复后,Hermes 开始执行日历流程,却收到:
NOT_AUTHORIZED
Calendar access not granted系统设置中的“隐私与安全性 → 日历”已经显示“终端”和“python”拥有完全访问权限,但后台仍然失败。
这里有三个容易混淆的点。
1. “添加日程”不等于“完全访问”
只创建事件可能只需要添加权限,但我的流程在创建前还要读取已有事件、检查重复和时间冲突,因此必须授予“完全访问”。只允许添加事件不足以执行完整流程。
2. 终端和后台 Gateway 是不同的权限上下文
在终端运行 accli calendars --json 成功,只能证明终端启动链拥有权限。由 launchd 启动的 Hermes Gateway 可能被 macOS 识别为另一个权限主体,界面中经常显示为 python。
3. 看不到 osascript 不一定异常
macOS 的日历权限列表不是手动添加清单。系统只显示实际发起过权限请求并被识别为责任主体的程序。accli 内部通过 Node.js 启动 osascript,再使用 JXA 和 EventKit;最终显示的名称可能是终端、python 或其他责任进程,因此不必执着于寻找名为 osascript 的项目。
我的排查顺序如下。
首先重启 Gateway,使刚修改的权限生效:
hermes gateway restart然后通过 Telegram 让 Hermes 只执行只读测试:
请只运行 accli calendars --json,告诉我是否成功,不要创建或修改任何日程。如果仍失败,就停止后台服务并以前台方式运行:
hermes gateway stop
cd ~/Hermes-Workspace
hermes gateway run再通过 Telegram 执行同一个只读测试:
- 前台成功、后台失败:说明是 launchd 后台权限上下文问题;
- 前台也失败:说明日历授权记录本身有问题。
如果关闭并重新开启 python 的日历完全访问权限仍然无效,可以重置当前用户的日历授权:
tccutil reset Calendar这条命令会清除应用的日历授权记录,但不会删除日历或事件。然后重启 Gateway,并在 Mac 解锁状态下从 Telegram 要求后台进程发起权限申请:
请立即运行 accli setup,等待 macOS 的日历授权结果,并把结果告诉我。系统出现弹窗后选择“允许完全访问”,再运行 accli calendars --json 验证。确认后台能够列出日历后,再恢复创建流程。
十、最终的验证顺序
不要一上来就用复杂日程测试。分层验证更容易定位问题。
第一步:检查 Gateway
hermes gateway status第二步:检查模型和 Telegram
在 Telegram 中发送:
/new随后发送:
只回复“连接正常”。第三步:检查后台日历读取权限
请只运行 accli calendars --json,不要创建或修改日程。第四步:检查带提醒的创建脚本
hermes-calendar-create \
"测试日程" \
"2026-09-14T15:00" \
"2026-09-14T15:30" \
"5" \
"" \
"Hermes 日历功能测试"确认返回 JSON 中包含:
{
"ok": true
}然后打开苹果日历,检查标题、起止时间、目标日历和提前五分钟提醒。
第五步:验证完整自然语言流程
在 Telegram 中发送:
帮我安排明天下午三点到四点半在图书馆复习高数,提前二十分钟提醒。完整成功标准包括:
- Hermes 正确解析明确日期;
- 能读取“
Hermes 日程”并检查重复和冲突; - 没有冲突时直接创建;
- 事件进入正确日历;
- 开始和结束时间正确;
- 提醒为提前二十分钟;
- 地点为图书馆;
- Hermes 只在工具返回成功后报告创建成功。
十一、后台运行和笔记本休眠
Gateway 作为 launchd 服务运行后,可以关闭终端,也会在用户登录 Mac 后自动启动。常用命令如下:
hermes gateway status
hermes gateway start
hermes gateway stop
hermes gateway restart
hermes gateway uninstall查看日志:
tail -f ~/.hermes/logs/gateway.log
tail -f ~/.hermes/logs/gateway.error.logMac 锁屏不会停止 Gateway,但系统真正休眠后,Gateway 会暂停,Telegram、微信和定时任务都不能实时运行。临时需要锁屏后保持运行时,可以在另一个终端执行:
caffeinate -i保持盖子打开后锁屏即可;普通情况下合盖仍会使 Mac 休眠。已经写入苹果日历的提醒由系统日历管理,不依赖 Gateway 持续在线。
十二、最后总结
这次真正打通的并不是一个命令,而是五层能力:
- 模型层:OpenAI Codex 能在 Gateway 后台环境中稳定连接;
- 消息层:Telegram 或微信能够接收指令并返回结果;
- 推理规则层:Hermes 能提取必要字段、统一追问并防止误创建;
- 查询层:accli 获得日历完全访问,用于查重和冲突检查;
- 写入层:自定义 JXA 脚本创建带一个或多个提醒的 Apple Calendar 事件。
最容易踩坑的地方也很明确:
- 为轻量工具误装完整 Xcode;
- 假设当前版本的命令支持
--alert; - 把代理只配置在终端函数中;
- 忽略 launchd 的 PATH 和工作目录;
- 把终端权限当成后台 Gateway 权限;
- 只授予“添加日程”而没有授予“完全访问”;
- 修改
.hermes.md后继续沿用旧 Session; - 模型连接失败时误以为是日历脚本故障。
完成这些配置后,我已经可以从 Telegram 给 Hermes 发送一句自然语言,让它在信息不足时先追问,在信息完整后检查冲突,并把带提醒的日程写入苹果原生日历。这个流程也为后续接入苹果提醒事项、学习计划、课程资料整理和定时推送打下了基础。