# 容器取证问答:agent-node-lj-test - 时间:2026-11-13T13:42:55.029Z - 房间:`!tUqC0mcjJ-M1BdMPlOTpbcKDc3kI3aRGrlBqvZGe1eI`(临时房间,可保留查阅) - 消息统计:正文 46 段 / 工具过程 12 段 / 文件 0 个 - 结束原因:收到 ✅ reaction ## 我发出的问题 ```text ## 补充一条你可能没注意到的关键线索 我在历史取证里翻到了这个容器**所有成功插件的注册方式**: | 插件 | 注册的 hook | |---|---| | `plugin-active-reporter` | `pre_llm_call` + `post_llm_call` | | `plugin-topup-guard` | `pre_llm_call` | | `plugin-tips-cn` | `transform_tool_result` + `transform_llm_output` + `pre_gateway_dispatch` | | `plugin-deliverable-inject` | `transform_llm_output` | | `plugin-fmode` | `on_session_end` | **规律:全都是靠 `ctx.register_hook(...)` 注册了真实 hook 才被实载。** 你的 `plugin-matrix-process-notice` 是 `hooks: []` + 纯 monkey-patch,**没有任何 hook 注册** → loader 跳过它。 --- ## 但先别急着改成 hook 方式,还有个更直接的问题要确认 **`post_tool_call` shell hook 已经存在**(`config.yaml:63-69`): ```yaml post_tool_call → python3 /opt/data/scripts/fmode-deliverable-hook.py matcher: terminal|write_file|patch|execute_code ``` **每次工具调用都会 fork 一个 Python 进程跑这个脚本!** 请检查:`fmode-deliverable-hook.py` 里在 `post_tool_call` 时能不能**拿到工具名和参数**?如果能,直接在它里面加一段「往 Matrix 发一条过程消息」,**根本不需要写新插件**。 这比折腾插件加载机制简单得多,而且这个脚本已经被证明能跑。 --- ## 请按这个顺序做 ### 1. 先查现成脚本(最快路径) ```bash grep -n "post_tool_call\|def main\|tool_name\|payload" /opt/data/scripts/fmode-deliverable-hook.py | head -20 ``` 回答:`post_tool_call` 分支里能拿到 tool_name / tool_input 吗? ### 2. 如果能拿到 → 直接在里面加发消息逻辑 - 用已有的 Matrix 发送能力(你的 outbound 插件里就有) - 发一条 `💻 terminal · ` 的消息 - 带 `fmode.kind: 'process'` ### 3. 如果拿不到 → 再用 hook 方式改造插件 照 `plugin-tips-cn` 的写法,注册一个真实 hook(`transform_llm_output` 或 `post_tool_call` 插件 hook),在 hook 里做 patch。 ### 4. 无论走哪条路,验证方式不变 重启 → 跑 3 次工具调用 → 回读 `fmode.kind` --- **核心判断**:你之前一直在修插件内部逻辑,但插件压根没被加载。要么用现成的 shell hook(最简单),要么按 tips-cn 的方式让它被加载。 ``` ## 容器回答(原文,按时间顺序,未删改) ### 1【prose】 ```text 我先看一眼现成脚本,把事实核实清楚再动手。 ``` ### 2【prose】 ```text ``` grep -n "post_tool_call\|def main\|to... ``` 📖 Reading fmode-deliverable-hook.py L250-349 ``` ### 3【prose】 ```text 拿得到,`tool_name` 和 `tool_input` 都在 payload 里。我看一下这段的完整实现和现有的 Matrix 发送能力。 ``` ### 4【prose】 ```text 路径清楚了:`post_tool_call` 能拿到 `tool_name` 和 `tool_input`。现在我需要 Matix 的发送凭据,看看现有 outbound 插件怎么发。 ``` ### 5【prose】 ```text 凭据就在 `.env` 里:`MATRIX_HOMESERVER`、`MATRIX_ACCESS_TOKEN`。我先验证一下 `post_tool_call` 的 payload 里是否真的带上房间信息,否则脚本不知道该发到哪个房间;这一步是关键。 ``` ### 6【prose】 ```text ``` grep -n "MATRIX" /opt/data/.env 2>/de... ``` ``` cd /opt/data && python3 - <<'EOF' ... ``` ``` ### 7【prose】 ```text 现在我直接抓取 `post_tool_call` 的真实 payload 结构,这样就知道脚本里能拿到哪些字段,尤其是房间号。 ``` ### 8【prose】 ```text 现在临时把 `post_tool_call` 的 hook 命令换成这个探针,跑一次工具调用,看真实 payload。这一步只要几秒,验证完马上还原。 ``` ### 9【prose】 ```text ``` cd /opt/data && python3 - <<'EOF' ... ``` ``` ### 10【prose】 ```text 探针已经就位。触发一次工具调用,看看真实 payload 里的字段。 ``` ### 11【prose】 ```text ``` echo "trigger probe" ``` ``` ### 12【prose】 ```text ``` ls -la /tmp/probe/; echo "=== payload... ``` ``` ### 13【prose】 ```text 探针没有被调用,说明 config 修改需要重启才能生效。我先还原配置,改用更可靠的方法:直接查看 Hermes 源码中 `post_tool_call` 的触发方式。 ``` ### 14【prose】 ```text * ``` ls -la /tmp/probe/; echo "=== payload... ``` ``` cd /opt/data && python3 - <<'EOF' ... ``` ``` ### 15【prose】 ```text 配置已还原。现在直接读 Hermes 源码,确认 `post_tool_call` 的 payload 究竟包含哪些字段;这是最权威的依据,不用猜测。 ``` ### 16【prose】 ```text ``` python3 -c "import hermes, os; print(... ``` 🔎 Searching files for post_tool_call|tool_name|tool_input|s... ``` ### 17【prose】 ```text 已经确认 `tool_name`、`tool_input`、`session_id` 都在 payload 里,但**没有 chat_id 或房间号**。这些需要从 `state.db` 按 session_id 反查。同时还要确认 hook 携带哪些 extra 字段。 ``` ### 18【prose】 ```text 已经确认可行:`session_id` 可以从 `sessions.chat_id` 反查房间号,`tool_name` 和 `tool_input` 都能拿到。现在开始写一个独立的发送模块,避免影响现有成果上传逻辑。 ``` ### 19【prose】 ```text 关键发现有两个:第 550 行显示,**Hermes 已经通过环境变量 `HERMES_SESSION_CHAT_ID` 把房间号传入 hook 子进程**,因此直接读取这个环境变量,比查询数据库更可靠。 再看一下旧插件里「kind」用的取值约定,确保新实现与 App 侧保持一致。 ``` ### 20【prose】 ```text ``` grep -rn "fmode.kind\|process_notice\... ``` 📖 Reading __init__.py L130-199 ``` ### 21【prose】 ```text 环境变量在 `gateway/session_context.py` 中管理,在 gateway 进程内可用。为了确认它在 `post_tool_call` 子进程里是否真的存在,我直接实测一次;这次用正确的探针,并确认 shell hook 是否需要在启动时注册。 ``` ### 22【prose】 ```text 探针已经就绪。现在把 `post_tool_call` 临时指向它;这次需要让 Hermes 重新加载配置才能生效。我先确认进程和配置重载机制。 ``` ### 23【prose】 ```text ``` ps -eo pid,ppid,etime,cmd | grep -i "... ``` ``` ### 24【prose】 ```text 已经找到 gateway 进程(116634)。为了避免反复重启影响主人,我改用更稳妥的方式:先只在这份脚本里验证环境变量是否存在,同时把发送逻辑写好,然后一次性完成验证。 接下来先做最关键的一步:用真实的 payload 结构测试脚本。 ``` ### 25【prose】 ```text ``` cd /tmp/probe && python3 - <<'PYEOF' ... ``` ``` ### 26【prose】 ```text 结论明确:`tool_name`、`tool_input` 和 `session_id` 都能拿到;房间号可以通过 `HERMES_SESSION_CHAT_ID` 环境变量或查询 `state.db` 获得。现在开始写发送模块,并把它接入现有脚本。 ``` ### 27【prose】 ```text ``` cd /tmp && python3 - <<'PYEOF' ... ``` ``` ### 28【prose】 ```text 凭据可以正常使用。现在编写独立的发送模块,并把它接入现有脚本的 `post_tool_call` 分支。 ``` ### 29【prose】 ```text 先手动自检:直接向这个房间发送一条消息,验证 Matrix 凭据和 `fmode.kind` 是否能真正写入。 ``` ### 30【prose】 ```text 🧪 terminal · 过程消息自检 (fmode.kind=process) ``` ### 31【prose】 ```text 消息已经发送成功(event_id: `$X_xR4NH...`)。现在回读那条事件,确认 `fmode.kind` 确实真的写入了,而不是仅仅把 body 发送出去。 ``` ### 32【prose】 ```text ``` cd /opt/data && python3 - <<'PYEOF' ... ``` ``` ### 33【prose】 ```text 从服务器回读确认:`fmode.kind = 'process'` 确实存在于事件 content 中。这是关键验证,之前插件一直做不到。 现在把它接入现有的 `post_tool_call` 分支。 ``` ### 34【prose】 ```text 还需要补上两处:引入 `subprocess` 模块;另外,`HERMES_SESSION_CHAT_ID` 不在 payload 里,我应主动从进程环境中读取并透传。 ``` ### 35【prose】 ```text patch · /opt/data/scripts/fmode-deliverable-hook.py ``` ### 36【prose】 ```text 现在用真实的 payload 模拟一次完整的 hook 调用,验证会不会真的发出消息;这是不打真实命令、不重启的端到端测试。 ``` ### 37【prose】 ```text terminal · git status --short --branch ``` ### 38【prose】 ```text terminal · cd /opt/data && rm -f /opt/data/cache/process-notice/* 2>/de… ``` ### 39【prose】 ```text 端到端测试已经跑通(耗时 0.372 秒,没有拖慢工具链),并且**真实运行环境已经在自动发送过程消息**,因为刚才我自己发出的这几条工具调用已经触发并成功发送。 现在回读最新那条消息的事件,确认 `fmode.kind` 字段是否正确。 ``` ### 40【prose】 ```text ``` cd /opt/data && python3 - <<'PYEOF' ... ``` ``` ### 41【prose】 ```text 从服务器回读后确认:`fmode.kind = 'process'` 确实已经写入,而且触发机制是**真实的工具调用**,不是手工拼出来的 payload。 现在还需要确认一件事:在 Hermes 真实进程里,房间号是怎么拿到的,也就是该走环境变量还是查数据库,以及是否会出现跨房间误发。刚才日志显示有两个房间都在发送,这符合预期:不同 session 对应不同房间;但我想确认没有把消息发错地方。 ``` ### 42【prose】 ```text ``` ps -p 116634 -o pid= >/dev/null && tr... ``` ``` ### 43【prose】 ```text 两个房间都是真实会话,没有误发。最后用 3 次真实工具调用做验收测试,并统计 `fmode.kind`。 ``` ### 44【prose】 ```text ``` cd /opt/data && python3 - <<'PYEOF' ... ``` ``` echo "verify-run-1"; sleep 0.2; echo ... ``` ``` ### 45【prose】 ```text ``` cd /opt/data && python3 - <<'PYEOF' ... ``` ``` ### 46【prose】 ```text 回读结果暴露了一个重要事实,我先把情况说清楚,再做必要的收尾。 **核心结论:`post_tool_call` 这条路走通了,`fmode.kind=process` 已经真实写入,并且是从服务器回读确认的,不是本地推断。** **已确认的事实(均可回读)** - `post_tool_call` 的 payload 包含 `tool_name` 和 `tool_input`(来自 `/opt/hermes/agent/shell_hooks.py:87-88` 的源码,以及实测 dump)。 - 但**不包含房间号**。房间号要么来自 `HERMES_SESSION_CHAT_ID` 环境变量,要么按 `session_id` 查询 `state.db` 的 `sessions.chat_id`。 - 因此不用写新插件,直接在已有的 `fmode-deliverable-hook.py` 里加逻辑即可,你的判断是正确的。 **我做了什么** - 新建 `/opt/data/scripts/fmode-process-notice.py`:负责发送 `m.room.message`,在 content 里写入 `fmode.kind`,并做 60 秒去重,失败时静默处理。 - 在 `handle_post_tool_call` 里调用它,用 `Popen` 异步派生(实测整轮 hook 耗时 **0.372s**,没有拖慢工具链)。 **回读验证(原文)** - `body: terminal · git status --short --branch` → `fmode.kind = 'process'`,sender `@zgdcmgnqcd:fmode.cn` - 这个 `process` 事件来自真实工具调用触发,不是手工拼出来的 payload。 **同时暴露了一个必须告诉你的问题** 最近 10 条窗口里,我发出的**正文回复仍然是 `fmode.kind = `**: ``` 端到端测试已经跑通(耗时 0.372 秒… 从服务器回读后确认:`fmode.kind = 'process'`… ``` ``` 也就是说:**过程消息这一侧已经解决,但终态回复(final)这一侧仍然没有 `fmode.kind`**。原因是 shell hook 无法修改 Matrix 发送时的 content,这正是你之前判断 `plugin-matrix-process-notice` 应该做的事,而它因为 `hooks: []` 没被加载。 所以现在的准确状态是:**过程行 ✅ 真发且带 kind;终态行 ❌ 仍是 ``**。要让终态也带上 `fmode.kind: final`,还是得按 `plugin-tips-cn` 的方式注册真实 hook,或者重载那个插件。 我是否现在就把 `plugin-matrix-process-notice` 改造成带 `ctx.register_hook(...)` 的形式,补上终态这一半?那一步需要重启 gateway,我会先问你,再动手。 ``` ### 47【ack】 ```text ✅ 容器已标记完成 ``` --- ## 工具过程(仅供核对,非回答内容) - `💻 terminal ``` ls -la /opt/data/scripts/fmode-delive... ```` - `💻 terminal ``` ls /opt/data/plugins/ 2>/dev/null; ec... ``` ``` cd /opt/data/plugins/plugin-matrix-ou... ```` - `📖 Reading fmode-deliverable-hook.py L1-60 💻 terminal ``` mkdir -p /tmp/probe && cat > /tmp/pro... ```` - `💻 terminal ``` grep -rn "post_tool_call" /opt/hermes... ``` 📖 Reading shell_hooks.py L75-119` - `💻 terminal ``` cd /opt/data && python3 - <<'EOF' ... ``` ``` grep -n "MATRIX_HOMESERVER\|MATRIX_AC... ```` - `💻 terminal ``` grep -rn "HERMES_SESSION_CHAT_ID\|HER... ``` ``` cat > /tmp/probe/dump.py <<'PYEOF' ... ```` - `✍️ Writing /opt/data/scripts/fmode-process-notic...` - `💻 terminal ``` cd /opt/data && timeout 20 python3 /o... ```` - `🔧 Editing /opt/data/scripts/fmode-deliverable-h...` - `💻 terminal ``` grep -n "^import subprocess" /opt/dat... ```` - `🔧 Editing /opt/data/scripts/fmode-deliverable-h...` - `* 🔧 Editing /opt/data/scripts/fmode-deliverable-h... (×2) 💻 terminal ``` cd /opt/data && rm -f /opt/data/cache... ````