Furthe77oad
Furthe77oad
发布于 2026-09-13 / 0 阅读
0
0

在 macOS 上打通 Hermes Agent 与 Apple 日历:从一句话创建日程到后台运行的完整踩坑记录

我希望把 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 必须知道:

  1. 标题;
  2. 日期;
  3. 开始时间;
  4. 结束时间或持续时长;
  5. 提前多久提醒,或者明确不提醒。

地点和备注是可选项,用户没说就不用追问。如果缺少多个必要信息,应当一次问完,不能一次只问一个。信息完整、没有冲突时直接创建,不需要再次确认;修改和删除则必须先展示变化并征得确认。

例如:

帮我安排明天下午三点到四点半在图书馆复习高数,提前二十分钟提醒。

这句话已经包含全部必要信息,可以直接进入冲突检查和创建流程。

而下面这句话信息不完整:

明天下午提醒我复习高数。

Hermes 应一次性询问具体开始时间、持续时长以及提前多久提醒,不能自行假设。

三、第一个问题:Homebrew 和完整 Xcode

最初我尝试安装另一个 Apple Calendar CLI:

brew install sichengchen/tap/apple-calendar-cli

一开始终端甚至没有 brew 命令。安装 Homebrew 后,又遇到了两个提示:

  1. 第三方 tap 尚未被 Homebrew 信任;
  2. 这个工具需要完整的 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/codex

Telegram 也出现:

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.md

Hermes 会从会话工作目录发现项目规则。如果 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.log

Mac 锁屏不会停止 Gateway,但系统真正休眠后,Gateway 会暂停,Telegram、微信和定时任务都不能实时运行。临时需要锁屏后保持运行时,可以在另一个终端执行:

caffeinate -i

保持盖子打开后锁屏即可;普通情况下合盖仍会使 Mac 休眠。已经写入苹果日历的提醒由系统日历管理,不依赖 Gateway 持续在线。

十二、最后总结

这次真正打通的并不是一个命令,而是五层能力:

  1. 模型层:OpenAI Codex 能在 Gateway 后台环境中稳定连接;
  2. 消息层:Telegram 或微信能够接收指令并返回结果;
  3. 推理规则层:Hermes 能提取必要字段、统一追问并防止误创建;
  4. 查询层:accli 获得日历完全访问,用于查重和冲突检查;
  5. 写入层:自定义 JXA 脚本创建带一个或多个提醒的 Apple Calendar 事件。

最容易踩坑的地方也很明确:

  • 为轻量工具误装完整 Xcode;
  • 假设当前版本的命令支持 --alert;
  • 把代理只配置在终端函数中;
  • 忽略 launchd 的 PATH 和工作目录;
  • 把终端权限当成后台 Gateway 权限;
  • 只授予“添加日程”而没有授予“完全访问”;
  • 修改 .hermes.md 后继续沿用旧 Session;
  • 模型连接失败时误以为是日历脚本故障。

完成这些配置后,我已经可以从 Telegram 给 Hermes 发送一句自然语言,让它在信息不足时先追问,在信息完整后检查冲突,并把带提醒的日程写入苹果原生日历。这个流程也为后续接入苹果提醒事项、学习计划、课程资料整理和定时推送打下了基础。

参考资料


评论