更新时间:2026-02-13
适用版本:OpenCodev1.1.53,Hermes 当前实现
在 Telegram 远程场景下,为 OpenCode Question Tool 提供可维护的回答回传能力,使用官方 API 完成闭环,不依赖异常通道注入。
hermes-hook.js
监听tool.execute.before(仅question),将问题推送到 Telegram。permission-listener.js
监听 Telegram 回调与文本消息,将用户答案回传 OpenCode Question API。pending-store.js
跨进程状态缓存(/tmp/hermes-pending.json),保存sessionID/callID/options等路由信息。
- Method:
GET - Path:
/question - Query:
directory=<project_dir> - 返回关键字段:
id(requestID)sessionIDtool.callIDquestions[](问题与选项)
- Method:
POST - Path:
/question/{requestID}/reply - Query:
directory=<project_dir> - Body:
{
"answers": [["A"]]
}说明:
answers是二维字符串数组。- 当前实现单题单选,使用第一项字符串作为答案。
- Method:
POST - Path:
/question/{requestID}/reject - Query:
directory=<project_dir>
- Agent 调用
question工具。 hermes-hook.js读取input.callID、sessionID、问题选项,写入 pending store。hermes-hook.js将按钮消息发送到 Telegram(qopt:*/qcustom:*)。- 用户点击按钮或输入文本。
permission-listener.js根据uniqueId读取 pending。- Listener 调用
GET /question,按callID + sessionID匹配 requestID。 - Listener 调用
POST /question/{requestID}/reply。 - 成功后编辑 Telegram 消息并清理 pending。
- OpenCode question tool 状态变为
completed,会话继续执行。
- 用户点击
qcustom:<uniqueId>。 - Listener 将 pending 标记为
awaitingText=true,并发送force_reply提示消息。 - 用户必须 reply 这条提示消息输入文本答案。
- Listener 优先用
reply_to_message.message_id匹配customPromptMessageId。 - 匹配成功后执行
/question/{requestID}/reply,并清理 pending。
回退逻辑:
- 若没有 reply 关系,仍可按“最近活跃 question + 文本消息”做回退匹配。
- 但在并发会话下存在误命中风险,因此文档与交互均推荐 reply 模式。
type=question 关键字段:
sid:sessionIDcallID:tool callIDdirectory:项目目录(用于/question查询)options:按钮选项(label/value)chatId/messageId:用于回写 Telegram 消息awaitingText:自定义输入等待标记timestamp:TTL 清理依据
TTL:
QUESTION_TTL_MS = 30min- 过期时尝试调用
/question/{requestID}/reject
requestID 解析策略(按优先级):
- 已缓存
requestID时直接使用。 - 使用
callID精确匹配question.tool.callID。 - 回退:同 session 的唯一 question。
重试策略:
- 默认最多 20 次
- 每次间隔 300ms
- 总等待窗口约 6 秒
GET /question失败:记录错误并提示 Telegram 用户“回传失败,请稍后重试”。- 匹配不到 requestID:视为暂态失败,不删除 pending。
reply非 2xx:回传失败,保留上下文并输出错误消息。reject失败:仅告警,不影响主轮询。
补充(自定义回答):
- 用户把文本发给业务机器人(如
@Napsta...)而非 reply Permission Bot,会被当作普通任务转发,question 保持 pending。 - 群组启用隐私模式且未正确放行消息时,Listener 只能收到 callback,收不到文本消息。
- 出现
fetch failed时先区分链路:若 callback 已成功回传 question,则该错误通常为非致命网络抖动。
hermes-hook.js不阻塞 question 工具执行,不抛出业务异常。- 当前回答模型为单题/单值;若启用多题或多选,需扩展
answers组装逻辑。 - 并发多个 question 依赖
callID稳定性;上游字段变化需同步调整匹配器。
curl -s "http://127.0.0.1:4096/question?directory=$PWD" | jq '.'- question 列表是否包含
id/sessionID/tool.callID。 - 回答后 question 是否从列表移除。
- 对应 assistant message 的 tool 状态是否为
completed。
以下日志可作为最小证据链:
- 收到按钮:
[PermListener] 📥 收到 callback_query: data=qcustom:<id> - pending 命中:
[PermListener] 📋 getPending(<id>): type=question - 收到文本:
[PermListener] 📨 收到 message: ... replyTo=<promptMessageId> - 回传成功:
[PermListener] ✅ question 已回传 OpenCode: requestID=...
/Users/napstablook/.config/opencode/HERMES/opencode/hermes-hook.js/Users/napstablook/.config/opencode/HERMES/opencode/lib/permission-listener.js/Users/napstablook/.config/opencode/HERMES/opencode/lib/pending-store.js/Users/napstablook/.config/opencode/HERMES/README.md