众神的信使 — OpenCode ↔ OpenClaw ↔ Telegram 双向通信桥梁
Hermes 让你通过 Telegram 远程控制 OpenCode TUI,不在电脑前也能发需求、审批权限、接收进度通知。
┌─────────────┐ ┌──────────────────┐ ┌──────────────┐
│ Telegram │ ◄─────► │ OpenClaw │ ◄─────► │ OpenCode │
│ (你的手机) │ │ Gateway :18789 │ │ TUI :4096 │
└─────────────┘ └──────────────────┘ └──────────────┘
▲ │ │
│ Hermes Agent hermes-hook.js
│ (SOUL.md) (插件)
│
│ ┌──────────────────────┐
└────│ Permission Bot │◄── permission-listener.js
│ (直发 Telegram) │ (长轮询 callback_query + message)
└──────────────────────┘
方向 A — 用户 → OpenCode: 你在 Telegram 发消息 → OpenClaw Hermes Agent 通过 prompt_async 转发到 OpenCode
方向 B — OpenCode → 用户(双路径):
- 权限请求 + 通知 + 问题:
hermes-hook.js→ Permission Bot → 直发 Telegram(绕过 Agent,避免上下文丢失) - 回退路径:Permission Bot 未配置时,走 OpenClaw Agent webhook(
deliver: true)
权限回调: 用户点击 Telegram Inline Keyboard → permission-listener.js 长轮询接收 → 调用 OpenCode 权限 API
问题回调: Agent 提问时推送 Inline Keyboard 到 Telegram → 用户点击选项或输入自定义回答 → permission-listener.js 通过官方 Question API(/question + /reply)回传答案
- OpenCode — AI 编程 TUI,需启用 HTTP Server(默认
:4096) - OpenClaw — AI Agent 框架,需启用 Gateway + Telegram channel
- 一个 Telegram Bot(通过 @BotFather 创建)
- 一个 Telegram 群组(把 Bot 加进去)
HERMES/
├── opencode/
│ ├── hermes-hook.js # OpenCode 插件(方向 B)
│ └── lib/
│ ├── pending-store.js # 待处理请求存储(权限 + 问题,JSON 文件)
│ ├── control-state.js # 接管编排状态存储(模式/Agent/skill/进度)
│ ├── permission-listener.js # Telegram 回调监听(权限 + 问题,独立进程)
│ └── hermes-hook.test.js # 测试(Vitest + fast-check PBT)
├── openclaw/
│ ├── SOUL.md # Hermes Agent 行为指令
│ ├── TOOLS.md # Agent 工具使用指南
│ ├── USER.md # 用户信息模板
│ ├── HERMES_QUICKSTART.md # 快速启动
│ └── HERMES_REQUIREMENTS.md
└── docs/ # 开发文档
openssl rand -hex 32
# 输出类似: c92123915191b5177b9eba7e00aa38c7...编辑 ~/.openclaw/openclaw.json(或 ~/.config/openclaw/openclaw.json):
把 openclaw/ 目录下的文件复制到 Hermes Agent 的 workspace:
cp openclaw/SOUL.md /path/to/hermes-workspace/
cp openclaw/TOOLS.md /path/to/hermes-workspace/
cp openclaw/USER.md /path/to/hermes-workspace/
SOUL.md是 Agent 的核心行为指令,定义了消息路由、权限处理、转发规则等。按需修改其中的 Session ID 和群组 ID。
# 主插件
cp opencode/hermes-hook.js ~/.config/opencode/plugins/
# lib 目录(OpenCode 不递归扫描子目录,所以 lib/ 下的文件不会被当作插件加载)
mkdir -p ~/.config/opencode/plugins/lib
cp opencode/lib/pending-store.js ~/.config/opencode/plugins/lib/
cp opencode/lib/control-state.js ~/.config/opencode/plugins/lib/
cp opencode/lib/permission-listener.js ~/.config/opencode/plugins/lib/在 ~/.zshrc(或 ~/.bashrc)中添加:
export HERMES_HOOK_TOKEN="<和 openclaw.json hooks.token 一致>"
export HERMES_OPENCLAW_URL="http://localhost:18789"
export HERMES_TELEGRAM_CHANNEL="<你的群组 ID>"
export HERMES_PERMISSION_BOT_TOKEN="<Permission Bot Token(推荐,启用直发 Telegram)>"
# 可选:状态/存储文件(默认在 /tmp)
export HERMES_PENDING_STORE_PATH="/tmp/hermes-pending.json"
export HERMES_CONTROL_STATE_PATH="/tmp/hermes-control-state.json"
# 可选:文件权限(八进制)
export HERMES_PENDING_STORE_MODE="600"
export HERMES_CONTROL_STATE_MODE="660"然后 source ~/.zshrc。
⚠️ 环境变量必须在启动 OpenCode 之前生效,否则插件会静默禁用。
# 终端 1: 启动 OpenClaw Gateway
openclaw gateway start
# 终端 2: 启动 Permission Listener(处理 Telegram 按钮回调)
node ~/.config/opencode/plugins/lib/permission-listener.js
# 终端 3: 启动 OpenCode(在你的项目目录下)
opencodePermission Listener 是独立的 Node.js 长轮询进程,负责接收 Telegram Inline Keyboard 的回调(权限审批 + 问题回答)以及群组文本消息(自定义回答),并调用 OpenCode 相应 API。必须在 OpenCode 之前或同时启动。
在 Telegram 群组中发一条消息,Hermes Agent 应该会回复确认。
也可以手动测试 webhook:
curl -X POST http://localhost:18789/hooks/agent \
-H "Authorization: Bearer <你的 token>" \
-H "Content-Type: application/json" \
-d '{
"message": "测试消息",
"name": "Hermes",
"agentId": "hermes",
"sessionKey": "hermes-notifications",
"wakeMode": "now",
"channel": "telegram",
"to": "<你的群组 ID>"
}'为避免 HERMES/opencode 与 ~/.config/opencode/plugins 漂移,建议每次改动后执行:
bash HERMES/scripts/sync-runtime.sh
bash HERMES/scripts/check-plugins.sh每次更新 HERMES/opencode 后,建议按下面顺序执行:
# 1) 同步源码到运行目录
bash HERMES/scripts/sync-runtime.sh
# 2) 校验 plugins/workspace 与 HERMES 源码一致
bash HERMES/scripts/check-plugins.sh
# 3) 重启 Permission Listener(必须)
pkill -f "permission-listener.js" || true
node ~/.config/opencode/plugins/lib/permission-listener.js
# 4) 重启 OpenCode(让插件重新加载)
# 先退出当前 opencode,再重新启动如果不重启 permission-listener.js,新逻辑(模式/agent/skill/Question 自定义回答等)不会生效。
在 Telegram 群组中直接发消息,Hermes Agent 会转发到 OpenCode:
帮我创建一个 REST API
若 OpenClaw 群组配置了
requireMention=true,消息需要带@Napsta6100ks_bot才会触发。
用括号包裹的内容是跟 Agent 说的,不会转发:
(查一下当前 session)
(状态怎么样)
以下控制指令在群组发送即可(建议使用括号):
(模式:转发)(模式:协同)(模式:代决策)(接管: <目标>)(停止接管)(选择Agent)(skill:plan|execute|debug|review)
skill仅允许plan/execute/debug/review,无效值会直接报错,不再静默降级。
说明:
forward:原样转发copilot:任务封装转发 + 里程碑推进delegate:代推进一般步骤,但高风险权限仍必须按钮确认
(选择Agent) 会由 Permission Bot 弹出按钮菜单:
- 选择 oh-my-opencode Agent(来自
~/.config/opencode/oh-my-opencode.json) - 选择 superpowers skill profile(
plan/execute/debug/review)
当 OpenCode 需要执行敏感操作时,你会在 Telegram 收到带 Inline Keyboard 的消息:
🔴 *需要确认* [shell]
*命令:* `rm -rf node_modules`
*风险:* 🔴 high
点击下方按钮操作:
[🟢 RUN] [🔵 ALWAYS] [🔴 REJECT]
点击按钮即可,permission-listener.js 会自动调用 OpenCode API 执行对应操作。
如果 Permission Bot 未配置,会回退到旧的文本回复模式(通过 OpenClaw Agent)。
当 OpenCode Agent 使用 question tool 向用户提问时,你会在 Telegram 收到带选项按钮的消息:
❓ *Agent 提问*
*问题标题*
问题内容...
_点击下方按钮回答:_
[选项 A]
[选项 B]
[✏️ 自定义回答]
- 点击选项按钮:直接选择对应答案
- 点击「✏️ 自定义回答」:会收到 Permission Bot 的“请回复此消息输入回答”提示,必须在群组里回复该提示消息
- 也支持直接发送普通文本作为回退模式(按最近活跃问题匹配),但推荐优先使用“回复提示消息”的方式,避免误路由
从 OpenCode v1.1.53 开始,Question Tool 可以走官方 HTTP API 回传:
| API | 用途 |
|---|---|
GET /question |
拉取待回答问题(含 id、sessionID、tool.callID、题目与选项) |
POST /question/{requestID}/reply |
提交用户答案 |
POST /question/{requestID}/reject |
拒绝/超时关闭问题 |
当前实现流程:
hermes-hook.js在tool.execute.before仅负责把问题推送到 Telegram,不阻塞、不抛错。permission-listener.js收到 Telegram 按钮/文本回答后,通过callID + sessionID匹配GET /question返回的requestID。- 用
POST /question/{requestID}/reply提交答案(格式:{ "answers": [["A"]] })。 - OpenCode 将 tool 状态标记为
completed,并继续执行后续步骤。
这条链路不再依赖异常通道传值,避免了 throw Error 带来的语义和可维护性问题。
- 先点击「✏️ 自定义回答」。
- 等 Permission Bot 发出“请回复此消息输入回答”。
- 直接回复这条提示消息(reply),不要发给 Napsta 业务机器人。
- Listener 检测到
reply_to_message_id命中后,立即走/question/{requestID}/reply回传。
常见误操作:
- 在等待回答时发送
@Napsta...文本,会被当成普通任务转发到 OpenCode,而不是 question answer。 - 不回复提示消息、只发一条普通文本,可能被并发上下文抢占,表现为
QUEUED。
历史探索记录仍保留在
docs/question-tool-api-exploration_2026-02-11.md。
最新一次联调问题总结见:docs/问题排查_OpenCode队列阻塞与InvalidApiKey_2026-02-13.md。
Question Tool 官方回传技术文档见:docs/技术文档_QuestionTool官方API回传_2026-02-13.md。
联调关键发现与工程实践文档见:docs/技术文档_Hermes联调关键发现与工程实践_2026-02-13.md。
| 消息 | 含义 |
|---|---|
| 📋 PHASE_COMPLETE | OpenCode 完成一个阶段,附带 AI 回复摘要 |
| 🔴 需要确认 | 权限请求,等待你审批 |
| ❓ Agent 提问 | Agent 需要用户输入,带选项按钮 |
| ❌ ERROR | OpenCode 发生错误 |
权限消息和通知消息使用不同的 session,避免上下文污染:
hermes-permissions— 权限请求专用 sessionhermes-notifications— 通知消息专用 session
/tmp/hermes-pending.json 存储待处理的权限请求和问题,每条记录包含 type 字段区分类型:
| type | 用途 | 关键字段 |
|---|---|---|
permission |
权限审批 | sid, pid, command, risk |
question |
Agent 提问 | sid, callID, options, awaitingText |
TTL 30 分钟,原子写入(tmp+rename),重启后自动清理过期条目。
/tmp/hermes-control-state.json 用于管理编排模式和接管状态:
| 字段 | 含义 |
|---|---|
mode |
forward/copilot/delegate |
selectedAgent |
手选 oh-my agent(默认 sisyphus) |
selectedSkillProfile |
plan/execute/debug/review |
takeoverActive |
是否处于接管执行 |
takeoverGoal |
当前接管目标 |
lastProgressAt |
最近里程碑时间戳 |
activeSessionId |
绑定的 OpenCode session |
卡住策略:
- 默认 90 秒无进展告警(Telegram + 本地日志)
- 同阶段最多自动重投 1 次
- 重投后仍卡住则标记
blocked,等待人工决策
当用户在 Telegram 回答 Agent 提问时,完整流程如下:
1. hermes-hook.js 的 tool.execute.before 监听 question tool(不阻塞)
2. 发送问题到 Telegram(带 Inline Keyboard 选项按钮)
3. permission-listener.js 收到用户点击/文字回答
4. 通过 `GET /question` 按 `callID + sessionID` 匹配 requestID
5. 调用 `POST /question/{requestID}/reply` 回传答案
6. question tool 状态变为 completed,AI 按官方链路继续执行
7. 过期条目会尝试 `POST /question/{requestID}/reject` 关闭问题
历史 throw 方案见
docs/question-tool-api-exploration_2026-02-11.md。
- ✅ 已切换到官方 Question API 回传(
/question、/question/{requestID}/reply、/question/{requestID}/reject) - ✅
hermes-hook.js不再阻塞 question tool,也不再通过 throw 注入答案 - ✅
permission-listener.js使用callID + sessionID匹配 requestID 并提交答案 - ✅ README 与实现保持一致
快速自检:
curl -s "http://127.0.0.1:4096/question?directory=$PWD" | jq '.'当存在待回答问题时,返回应包含:
id(requestID)sessionIDtool.callID
插件会自动评估命令风险等级:
| 等级 | 匹配规则 | 示例 |
|---|---|---|
| high | rm -rf, dd, mkfs, chmod -R 777 |
rm -rf / |
| medium | rm, mv, sed -i, kill -9 |
rm file.txt |
| low | 其他所有命令 | echo hello |
hermes-hook.js 发送到 OpenClaw 的 payload 格式:
{
"message": "[HERMES_WEBHOOK — 转发给用户,不要自己处理] 消息内容",
"name": "Hermes",
"agentId": "hermes",
"sessionKey": "hermes-permissions | hermes-notifications",
"wakeMode": "now",
"deliver": true,
"channel": "telegram",
"to": "<群组 ID>"
}agentId 必须与 openclaw.json 中 agents.list[].id 一致。deliver: true 确保 OpenClaw 将 Agent 回复直接投递到目标群组。
| 变量 | 必填 | 默认值 | 说明 |
|---|---|---|---|
HERMES_HOOK_TOKEN |
✅ | — | OpenClaw webhook token |
HERMES_PERMISSION_BOT_TOKEN |
推荐 | — | Permission Bot token(启用直发 Telegram + Inline Keyboard) |
HERMES_OPENCLAW_URL |
— | http://localhost:18789 |
OpenClaw Gateway 地址 |
HERMES_TELEGRAM_CHANNEL |
— | -5088310983 |
Telegram 群组 ID |
HERMES_OPENCODE_PORT |
— | 4096 |
OpenCode HTTP Server 端口 |
编辑 openclaw/SOUL.md,主要可调整:
- 消息路由规则(括号约定)
- 权限处理流程
- 输出风格(极简 vs 详细)
- 环境信息(Session ID、模型等)
编辑 opencode/hermes-hook.js 中的 assessRisk() 函数,添加自定义的 high/medium 匹配规则。
在 hermes-hook.js 的 event handler 中添加新的 event.type 分支。OpenCode 支持的事件类型参考 OpenCode 插件文档。
- OpenCode HTTP Server 必须在本地运行(
localhost:4096) permission-listener.js必须作为独立进程运行(不能内嵌到 OpenCode 插件中)- Hermes Agent 使用的模型(如 MiniMax-M2.1)指令遵循能力有限,架构层面已做防护(权限和通知走直发 Telegram,不经过 Agent)
session.idle事件的消息获取依赖 OpenCode HTTP API,偶尔可能获取失败- 待处理请求存储在
/tmp/hermes-pending.json,TTL 30 分钟,重启后自动清理过期条目 - Question Tool 远程回答已迁移到官方
/questionAPI;如 API 行为变化,需要同步调整permission-listener.js的 requestID 匹配逻辑(callID + sessionID) - 自定义回答推荐使用“回复 Permission Bot 提示消息”;未 reply 时会走回退匹配,极端并发下仍存在误匹配风险
MIT
{ // Gateway 配置 "gateway": { "port": 18789, "mode": "local", "bind": "loopback" }, // 启用 webhook hooks "hooks": { "enabled": true, "token": "<你生成的 token>", "path": "/hooks" }, // Telegram 配置 "channels": { "telegram": { "enabled": true, "botToken": "<你的 Bot Token>", "groupPolicy": "allowlist", "groups": { "<你的群组 ID>": { "requireMention": false } } } }, // 注册 Hermes Agent "agents": { "list": [ { "id": "hermes", "name": "Hermes", "workspace": "/path/to/hermes-workspace" } ] }, // 绑定群组到 Hermes Agent "bindings": [ { "agentId": "hermes", "match": { "channel": "telegram", "peer": { "kind": "group", "id": "<你的群组 ID>" } } } ] }