fmodekind追查v4.md 13 KB

容器取证问答:agent-node-lj-test

  • 时间:2026-11-13T13:42:55.029Z
  • 房间:!tUqC0mcjJ-M1BdMPlOTpbcKDc3kI3aRGrlBqvZGe1eI(临时房间,可保留查阅)
  • 消息统计:正文 46 段 / 工具过程 12 段 / 文件 0 个
  • 结束原因:收到 ✅ reaction

我发出的问题

## 补充一条你可能没注意到的关键线索

我在历史取证里翻到了这个容器**所有成功插件的注册方式**:

| 插件 | 注册的 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 · <command 前 60 字>` 的消息
- 带 `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】

我先看一眼现成脚本,把事实核实清楚再动手。

2【prose】

grep -n "post_tool_call|def main|to...

📖 Reading fmode-deliverable-hook.py L250-349

3【prose】

拿得到,`tool_name` 和 `tool_input` 都在 payload 里。我看一下这段的完整实现和现有的 Matrix 发送能力。

4【prose】

路径清楚了:`post_tool_call` 能拿到 `tool_name` 和 `tool_input`。现在我需要 Matix 的发送凭据,看看现有 outbound 插件怎么发。

5【prose】

凭据就在 `.env` 里:`MATRIX_HOMESERVER`、`MATRIX_ACCESS_TOKEN`。我先验证一下 `post_tool_call` 的 payload 里是否真的带上房间信息,否则脚本不知道该发到哪个房间;这一步是关键。

6【prose】

grep -n "MATRIX" /opt/data/.env 2>/de...

cd /opt/data && python3 - <<'EOF' ...

7【prose】

现在我直接抓取 `post_tool_call` 的真实 payload 结构,这样就知道脚本里能拿到哪些字段,尤其是房间号。

8【prose】

现在临时把 `post_tool_call` 的 hook 命令换成这个探针,跑一次工具调用,看真实 payload。这一步只要几秒,验证完马上还原。

9【prose】

cd /opt/data && python3 - <<'EOF' ...

10【prose】

探针已经就位。触发一次工具调用,看看真实 payload 里的字段。

11【prose】

echo "trigger probe"

12【prose】

ls -la /tmp/probe/; echo "=== payload...

13【prose】

探针没有被调用,说明 config 修改需要重启才能生效。我先还原配置,改用更可靠的方法:直接查看 Hermes 源码中 `post_tool_call` 的触发方式。

14【prose】

* ```
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 = <none>:

<none>  端到端测试已经跑通(耗时 0.372 秒…
<none>  从服务器回读后确认:`fmode.kind = 'process'`…
<none>  ```

也就是说:过程消息这一侧已经解决,但终态回复(final)这一侧仍然没有 fmode.kind。原因是 shell hook 无法修改 Matrix 发送时的 content,这正是你之前判断 plugin-matrix-process-notice 应该做的事,而它因为 hooks: [] 没被加载。

所以现在的准确状态是:过程行 ✅ 真发且带 kind;终态行 ❌ 仍是 <none>。要让终态也带上 fmode.kind: final,还是得按 plugin-tips-cn 的方式注册真实 hook,或者重载那个插件。

我是否现在就把 plugin-matrix-process-notice 改造成带 ctx.register_hook(...) 的形式,补上终态这一半?那一步需要重启 gateway,我会先问你,再动手。


### 47【ack】

text ✅ 容器已标记完成 ```


工具过程(仅供核对,非回答内容)

  • 💻 terminalls -la /opt/data/scripts/fmode-delive...``
  • 💻 terminalls /opt/data/plugins/ 2>/dev/null; ec...cd /opt/data/plugins/plugin-matrix-ou...``
  • 📖 Reading fmode-deliverable-hook.py L1-60 💻 terminalmkdir -p /tmp/probe && cat > /tmp/pro...``
  • 💻 terminalgrep -rn "post_tool_call" /opt/hermes...📖 Reading shell_hooks.py L75-119
  • 💻 terminalcd /opt/data && python3 - <<'EOF' ...grep -n "MATRIX_HOMESERVER\|MATRIX_AC...``
  • 💻 terminalgrep -rn "HERMES_SESSION_CHAT_ID\|HER...cat > /tmp/probe/dump.py <<'PYEOF' ...``
  • ✍️ Writing /opt/data/scripts/fmode-process-notic...
  • 💻 terminalcd /opt/data && timeout 20 python3 /o...``
  • 🔧 Editing /opt/data/scripts/fmode-deliverable-h...
  • 💻 terminalgrep -n "^import subprocess" /opt/dat...``
  • 🔧 Editing /opt/data/scripts/fmode-deliverable-h...
  • * 🔧 Editing /opt/data/scripts/fmode-deliverable-h... (×2) 💻 terminalcd /opt/data && rm -f /opt/data/cache...``