| name | hermes-service-troubleshooting |
| description | 诊断 Hermes Gateway/API 服务异常的标准流程。当用户报告 gateway 没反应、API 挂了、公网不通时使用。 |
Hermes 服务故障诊断 SOP
触发条件
- 用户报告 gateway/API "没开"、"异常"、"连不上"
/health 或 ngrok 公网地址无响应
Phase 1: 快速摸底(30秒)
💡 标准审计清单: 使用 references/system-audit-checklist.md 中的全系统检查命令集,一次性摸清所有 gateway、守护进程、资源、配置状态。
ps aux | grep -i hermes | grep -v grep
launchctl list | grep -i hermes
ps aux | grep ngrok | grep -v grep
curl -s --max-time 5 http://127.0.0.1:4040/api/tunnels
Phase 2: 深度诊断 — launchd 与进程状态交叉验证
关键原则:launchd 的状态可能和实际进程状态不一致。必须交叉验证。
for svc in ai.hermes.gateway ai.hermes.gateway-her-m2 ai.hermes.gateway-english-tutor com.hermes.defibrillator com.hermes.network-watchdog com.hermes.system-watchdog; do
launchctl print gui/501/$svc
done
关注字段:
state — running vs spawn scheduled
last exit code — 0=正常退出, 1=异常
runs — 如果数字很大(如1563),说明在反复崩溃重启
pid — 交叉验证进程是否存活
stdout path / stderr path — 日志路径
Phase 3: 根因定位
常见故障模式
模式N:Weixin/微信 Token 冲突 — 多 gateway 争抢同一 token
- 症状:gateway 启动后日志出现
ERROR gateway.platforms.base: [Weixin] Weixin bot token already in use (PID XXXX). Stop the other gateway first.,微信平台无法连接
- 根因:Weixin token 是排他性资源——同一 token 只能被一个进程使用。当 her-m2 已占用 Weixin token 时,default 或 english-tutor 的 env/shell 环境泄漏了
WEIXIN_* 变量,导致它们也尝试连接微信
- 验证:检查各 profile 的
.env 和 launchd plist 的 EnvironmentVariables 中是否有 WEIXIN_* 变量。只有 her-m2 应该配微信
- 修复:
- 确保只有 her-m2 的
.env 含 WEIXIN_ACCOUNT_ID/WEIXIN_TOKEN
- 清理 default 和 english-tutor 的 shell 环境(launchd plist 不含
WEIXIN_* 即可,launchd 会隔离环境)
- 如果 shell 手动启动 gateway 导致泄漏:改用
launchctl kickstart 而非手动 HERMES_PROFILE=... gateway run
- 预防:launchd-managed gateway 自带环境隔离。手动启动时不要传
WEIXIN_* 环境变量
模式A:Port 冲突(API 端口被占)
- 症状:gateway 显示 exit code 1 + runs 很大
- 根因:手动启动的进程占着端口,launchd 反复尝试绑定失败
- 验证:
lsof -i :8642(API server 现在内嵌在 gateway,端口 8642 而非 18765)
- 修复:
kill <手动PID> → launchd 自动接管(KeepAlive 会自动重启)
模式B:Token/凭证缺失
- 症状:gateway 进程在跑但 Telegram 没反应
- 验证:检查对应 profile 的
.env 中是否有 TELEGRAM_BOT_TOKEN
模式C:Ngrok 隧道断连
- 症状:localhost API 正常但公网不通
- 验证:
curl http://127.0.0.1:4040/api/tunnels
- 修复:重启 ngrok 进程
模式D:Telegram SSL 证书验证失败(Bot 不响应但进程存活)
- 症状:gateway 进程在跑、Token 有,但发消息没反应。用户视角"bot卡住了"
- 根因:连到的 Telegram DC IP 返回证书不匹配(如
149.154.166.110),导致 [SSL: WRONG_VERSION_NUMBER] 或 [SSL: CERTIFICATE_VERIFY_FAILED]
- 验证:
tail -50 <gateway_log> | grep -i "ssl\|certificate\|error"
- 修复:重启 gateway → 自动换 Telegram endpoint → SSL 通过
- 如果重启无效:检查系统时间、代理、
REQUESTS_CA_BUNDLE/SSL_CERT_FILE 环境变量
模式E:DNS 污染导致 Telegram 连接超时 → SystemExit 75 反复崩溃
模式F:KeepAlive SuccessfulExit 陷阱 — gateway 被 SIGTERM 后永不自动重启
- 症状:launchd plist 有
KeepAlive → SuccessfulExit: false,gateway 收到 SIGTERM 后退出,launchd 认为"正常退出"不重启。服务从此消失
- 验证:
launchctl list | grep hermes — 缺少某个服务;日志末尾有 Received SIGTERM — initiating shutdown
- 修复:
launchctl bootstrap gui/501/<plist_path> && launchctl kickstart gui/501/<service>(见 Phase 4)
- 预防:如果 gateway 需要始终在线,将 KeepAlive 改为
<true/> 或追加 <key>Crashed</key><true/>
模式G:受保护配置文件修改失败 → 自杀循环(Protected Config Death Loop)
模式H:launchctl list exit code 误判 — 以为 gateway 挂了其实活着
- 症状:
launchctl list 显示某个服务的 exit code 为 -9/1/75,诊断者据此判断"gateway 死了",但实际 ps 显示进程在正常运行
- 根因:
launchctl list 的 exit code 列是【上一次进程退出时的返回码】,不是当前运行状态。当 launchd 的 KeepAlive 重启进程后,旧 exit code 依然显示
- 验证:不要只看
launchctl list,必须交叉验证:ps aux | grep hermes 或 kill -0 <PID>
- 记住:exit code -9 = 上次被 SIGKILL 过;exit code 0 = 上次正常退出;都不代表当前状态
- 正确判断方式:
kill -0 <PID> 2>/dev/null && echo "活着" || echo "死了"
模式I:Default profile 的 PID 文件不在常规路径
- 症状:监控脚本(如 hermes-monitor.sh)报告 default gateway 为"停止",但实际进程在跑
- 根因:default profile 的
gateway.pid 在 ~/.hermes/gateway.pid,而不是 ~/.hermes/profiles/default/gateway.pid
- 修复:监控脚本中为 default profile 设置特殊路径
模式J:Clash Verge fake-ip 模式全量 DNS 劫持 — 所有 DNS 查询被污染到 198.18.0.x
- 症状:gateway 反复出现
telegram connect timed out after 30s、httpx.ConnectError: All connection attempts failed,错误日志 900+ 条。dig @1.1.1.1 api.telegram.org 和 dig @8.8.8.8 api.telegram.org 全部返回 198.18.0.x
- 根因:Clash Verge 配置
dns-hijack: any:53 + enhanced-mode: fake-ip,拦截了所有 DNS 端口 53 查询(包括发往 1.1.1.1/8.8.8.8 的),全部导向 fake-ip 范围 198.18.0.1/16。Telegram API 域名解析为 fake IP,TCP 连接失败
- 验证:
- 确认 Clash 配置:
grep -E 'dns-hijack|enhanced-mode|fake-ip' ~/Library/Application\ Support/io.github.clash-verge-rev.clash-verge-rev/clash-verge.yaml
- 确认所有 DNS 被劫持:
dig @1.1.1.1 api.telegram.org +short 返回 198.18.x.x
- 确认种子 IP 可达(终极验证):
python3 -c "import socket; s=socket.create_connection(('149.154.167.220',443),timeout=5); s.close(); print('OK')"
- 修复(永久方案 — 需要 root/Touch ID):
- 编辑 Clash Verge 配置
~/Library/Application Support/io.github.clash-verge-rev.clash-verge-rev/clash-verge.yaml
- 在
rules: 第一个位置加入:- DOMAIN,api.telegram.org,DIRECT
- 在
dns: 下添加 nameserver-policy:"domain:api.telegram.org": 'https://dns.cloudflare.com/dns-query'
- 重启 Clash mihomo 核心:
sudo killall -9 verge-mihomo(Clash GUI 会自动重启它)
- 验证:再次
dig @1.1.1.1 api.telegram.org +short 应返回真实 IP
- 临时缓解(无需 root):
- 重启 gateway → 种子 IP 直连有时能绕过
- 重启 Clash Verge 整个 app(GUI 操作)
- 部署 network-watchdog 自动检测并重启 Clash
模式K:API Gateway 独立进程已废弃 — 旧 plist 引用不存在的模块
- 症状:
com.hermes.api-gateway 显示 exit code 1,tail ~/.hermes/logs/api-gateway-error.log 显示 ModuleNotFoundError: No module named 'tools.hermes_api'
- 根因:API server 已重构为 gateway 内嵌平台(
api_server platform),不再需要独立进程。旧 plist 引用已删除的 tools.hermes_api 模块
- 验证:
curl http://localhost:8642/health(gateway 内嵌 API 端口,非 18765)
- 修复:
- 清理旧 plist:
launchctl bootout gui/501/com.hermes.api-gateway
- 在 gateway 配置中启用 api_server:
config.yaml 中 api_server: enabled: true, extra: {host: 127.0.0.1, port: 8642}
- 当前唯一 API 入口:her-m2 gateway 的 8642 端口
模式L:launchd HardResourceLimits → RSS 导致 bootstrap 失败
- 症状:
launchctl bootstrap 报 Bootstrap failed: 5: Input/output error,gateway 无法通过 launchd 启动。defibrillator 反复尝试复活但每次都失败
- 根因:macOS launchd 的
HardResourceLimits 不支持 RSS 键。在 plist 中加入 <key>RSS</key><integer>1073741824</integer> 会导致 bootstrap 静默失败
- 验证:检查 plist 是否包含
HardResourceLimits → RSS,如果是则这是根因。可以创建不含 RSS 限制的临时 plist 验证:plutil -p /tmp/test.plist | grep RSS
- 修复:
- 从所有 plist 中移除
HardResourceLimits 整个块
- 内存监控改用 system-watchdog(每 5 分钟检查 gateway RSS,超过 500MB 告警)
- 重新 bootstrap:
launchctl bootstrap gui/501 <plist_path> && launchctl kickstart gui/501/<service>
- 预防:不要在任何 launchd plist 中使用 RSS 限制。macOS 不支持。支持的 key 仅限:
Core, CPU, Data, FileSize, MemoryLock, NumberOfFiles, NumberOfProcesses, ResidentSetSize, Stack
模式N:Defibrillator 误报"离线" — 进程存活但缺平台凭证
- 症状:
defibrillator.log 反复报告 [default] 离线但冷却中,跳过 或 ❌ 复活失败,进程可能未正常启动,巡检显示 活: ['her-m2', 'english-tutor'] | 死: ['default']。但 ps 和 launchctl list 确认进程 PID 存活、exit code 为 0。
- 根因:defibrillator 判断 gateway "存活"的标准不仅仅是 PID 存在,还包括平台连接状态。当 default gateway 的 Telegram 连接因
TELEGRAM_BOT_TOKEN 未配置而失败时(日志:[Telegram] No bot token configured),defibrillator 将其判为"离线"。复活尝试也因相同原因(token 仍然缺失)而失败 → 进入 15 分钟冷却期 → 反复报"离线但冷却中"。
- 验证:
ps aux | grep gateway — 确认 PID 存活
launchctl list | grep ai.hermes.gateway — 确认 launchd 状态正常(exit code 可能为 0 但 pid 列有值)
tail -20 <profile>/logs/gateway.log | grep -i 'telegram\|bot token' — 查看平台连接状态
tail -20 <profile>/logs/defibrillator.log — 查看误报模式
- 修复:
- 确认 profile
.env 中有 TELEGRAM_BOT_TOKEN=***
launchctl kickstart -k gui/501/ai.hermes.gateway 重启
- 验证日志中 Telegram 连接成功(不再有 "No bot token configured")
- 等待下一个 defibrillator 巡检周期(10秒),确认不再报"离线"
- 关键区分:进程存活 ≠ defibrillator 认为存活。当收到"Gateway X 自动复活 ❌"消息时,先检查该 gateway 的平台连接日志,不要直接假设进程挂了。
模式O:pkill 误杀其他 gateway(进程名不含 profile 名)
4a: launchd 服务恢复(当服务未加载时)
当 launchctl list 找不到服务(不是退出,是根本没加载),先 bootstrap 再 kickstart:
launchctl bootstrap gui/501 /Users/mac/Library/LaunchAgents/<service>.plist
launchctl kickstart gui/501/<service>
sleep 3 && launchctl list | grep <service>
tail -5 <profile>/logs/gateway.log
注意:kickstart 需要服务已 bootstrap,所以两步必须按顺序。gui/501 是 macOS 当前用户的 domain(id -u 确认 UID)。
4b: 服务验证
curl -s --max-time 5 http://localhost:18765/health
curl -s --max-time 10 https://<ngrok-domain>/health
launchctl list | grep hermes
kill -0 <PID>