← Back to Plugins
Tools

Chat Interceptor

caridina-ai By caridina-ai 👁 9 views ▲ 0 votes

OpenClaw plugin: hand each chat room's messages to a script before the agent replies, and fall through to the AI only when the script declines.

GitHub

Install

openclaw plugins install --link

Configuration Example

{
  "plugins": {
    "entries": {
      "openclaw-chat-interceptor": {
        "enabled": true,
        "hooks": { "allowConversationAccess": true },
        "config": {
          "debug": true,
          "routes": [
            {
              "name": "my-skill",
              "channel": "telegram",
              "target": "-1001234567890",
              "command": [
                "C:/Users/you/.local/bin/uv.exe",
                "run",
                "D:/projects/my-skill/my-script.py"
              ],
              "env": { "MY_SKILL_HOME": "D:/data/my-skill" },
              "timeoutMs": 30000
            },
            {
              "name": "another-skill",
              "channel": "discord",
              "target": "channel:123456789012345678",
              "command": [
                "C:/Program Files/nodejs/node.exe",
                "D:/projects/another-skill/index.mjs"
              ],
              "onError": "passthrough"
            }
          ]
        }
      }
    }
  }
}

README

# openclaw-chat-interceptor

一個 OpenClaw plugin:在 `before_agent_reply` hook 攔下聊天訊息,先交給該對話
對應的 script 處理。script 處理得了就直接回覆,**整輪不呼叫模型,0 token**;
script 說「我不會」時,訊息原封不動交還給 AI 走原本的流程。

適合固定格式、高頻出現、不需要語意判斷的指令——那些交給模型只是浪費 token。

另外兩件事也一併在這裡解決:使用者可以隨手打一則 [`ISSUE`](#issue) 記下哪裡不順手,
而 [`/tap`](#tap) 把整段使用過程汲取出來交給 AI 檢討——**包含那些被攔掉、
從來沒進過 session 的輸入**。

- **要寫 script 的人看這份**:[docs/AUTHORING.md](docs/AUTHORING.md)
- 內部設計(hook 機制、plugin 實作):[docs/DESIGN.md](docs/DESIGN.md)

裝一次,每個對話各自設一個 route。**plugin 只負責路由與呼叫,
本身不含任何領域邏輯**——那些屬於各自獨立的 script 專案。

## 安裝

```bash
git clone https://github.com/caridina-ai/openclaw-chat-interceptor
openclaw plugins install --link <path to this repo>
```

然後在 `~/.openclaw/openclaw.json` 設定。`allowConversationAccess` 是必要的:
非 bundled 的 plugin 沒有它就不能註冊 `before_agent_reply`。

```json
{
  "plugins": {
    "entries": {
      "openclaw-chat-interceptor": {
        "enabled": true,
        "hooks": { "allowConversationAccess": true },
        "config": {
          "debug": true,
          "routes": [
            {
              "name": "my-skill",
              "channel": "telegram",
              "target": "-1001234567890",
              "command": [
                "C:/Users/you/.local/bin/uv.exe",
                "run",
                "D:/projects/my-skill/my-script.py"
              ],
              "env": { "MY_SKILL_HOME": "D:/data/my-skill" },
              "timeoutMs": 30000
            },
            {
              "name": "another-skill",
              "channel": "discord",
              "target": "channel:123456789012345678",
              "command": [
                "C:/Program Files/nodejs/node.exe",
                "D:/projects/another-skill/index.mjs"
              ],
              "onError": "passthrough"
            }
          ]
        }
      }
    }
  }
}
```

改設定會 hot reload;改 `src/index.js` 才要 `openclaw gateway restart`。

> **前提**:Telegram 的 `groupPolicy` 預設是 `allowlist`。群組沒列在
> `channels.telegram.groups` 裡,訊息在 channel ingress 就被丟掉了,根本輪不到
> hook。新的房間要先加進去——而群組 id 正好可以從被丟掉時留下的那行 log 讀出來,
> 見 [DESIGN.md 2.5](docs/DESIGN.md)。

## 設定項

| 欄位 | 說明 |
|---|---|
| `debug` | 每一輪的判斷過程寫進 `logFile` |
| `logFile` | 預設 `~/.openclaw/logs/chat-interceptor.log` |
| `issueLogFile` | `ISSUE` 訊息寫到哪,預設 `~/.openclaw/logs/chat-interceptor-issues.log` |
| `routes[]` | 依序比對,第一個對上的勝出;都沒對上就整輪交給 AI |

每個 route:

| 欄位 | 必要 | 說明 |
|---|---|---|
| `name` | | trace 與錯誤訊息裡的標籤 |
| `channel` | | `telegram`、`discord`…;省略代表不限 channel |
| `target` | ✓ | 對話 id,寫法同 `openclaw message send --target` |
| `command` | ✓ | script 的 argv。**用絕對路徑**——gateway 是服務啟動的,PATH 跟 shell 不一樣 |
| `cwd` | | script 的工作目錄 |
| `env` | | 額外環境變數,蓋在 gateway 自己的之上 |
| `timeoutMs` | | 預設 15000 |
| `onError` | | `handle`(預設,回錯誤訊息)或 `passthrough`(交給 AI)。只有無副作用的 script 才適合 passthrough |

`target` 的三種寫法等價,都對得上執行時的裸 id:

```
telegram   -1001234567890      ≡  group:-1001234567890
                               ≡  telegram:group:-1001234567890
discord    123456789012345678  ≡  channel:123456789012345678
                               ≡  discord:channel:123456789012345678
```

## Script 協定

| | |
|---|---|
| stdin | JSON `{ body, channel, target, senderId, sessionKey, agentId }` |
| stdout | 要回給使用者的純文字;空的代表「處理掉但不回話」 |
| exit `0` | 處理掉了,0 token |
| exit `20` | 我不會,請交給 AI |
| 其他 | 出錯,依 `onError` 決定 |

`body` 是原樣的使用者輸入,plugin 不做任何 normalize——各 script 自己決定怎麼解讀。

用 stdin 而不是 argv 傳訊息:Windows 的 argv quoting 很容易出事,而且使用者的
訊息是不受信任的輸入。任何語言寫的 script 都能接。

完整的作者指南、可直接抄的範本、以及改寫既有 skill 的步驟,
見 [docs/AUTHORING.md](docs/AUTHORING.md)。

## ISSUE

<a id="issue"></a>

有 route 的房間裡,使用者可以隨手打一則 `ISSUE` 開頭的訊息,記下哪裡不順手:

```
ISSUE 按鍵盤之後要等好幾秒才有反應
```

plugin 自己吃掉:**不呼叫 AI、不傳給 script、不回話**。多行也可以,大小寫不拘。

認的就是 `ISSUE` 這個開頭,關鍵字後面接什麼就原樣記什麼
(`issues are piling up` 這種不算,那是一句話裡剛好有這個字)。

光靠 AI 答應「看到 ISSUE 就靜默略過」是不算數的:那句話還是進了 session、
還是叫醒了模型、還是花了 token。真正做得到的只有在模型之前就吃掉,也就是這裡。

被吃掉的訊息不進 session,所以它會被寫進 `issueLogFile`,一行一筆 JSON——
下面的 `/tap` 讀的就是這個檔。萬一寫不進去,那一則會回一句
`ISSUE not recorded: …`,不會默默吞掉使用者的待辦。

沒有 route 的房間不吃 `ISSUE`:那個房間沒套用本 plugin,攔下它等於偷走一句
本來會有人回應的話。

## `/tap`

<a id="tap"></a>

上線之後真正的問題是「哪裡還可以改」。但被攔掉的輸入不進 session,
OpenClaw 的 CLI 也沒有讀取頻道歷史的指令。`/tap` 是一個
[Agent Skill](https://github.com/vercel-labs/skills),把整段使用過程還原出來
交給 Claude Code 這類 AI harness 檢討:

```bash
npx skills add caridina-ai/openclaw-chat-interceptor
```

(只裝這一個 skill:`npx skills add caridina-ai/openclaw-chat-interceptor --skill tap`;
裝給特定 harness 加 `-a claude-code`,裝成全域加 `-g`。)

裝好之後在專案裡叫它:**「tap 一下我的記帳頻道,看看哪裡可以改」**。它會給你

- 使用者實際打過的每一句話,**包含被 script 攔掉、沒進 session 的那些**
- 每一句當時的下場:攔下了、放行給 AI、還是出錯了
  ——**「放行給 AI」那幾則就是下一條規則的原料**
- 使用者累積的 `ISSUE`

然後 AI 拿這些去改那支 script。汲取只是手段,改程式才是目的。

每個專案用一個 `.tap` 記住自己的頻道在哪裡,**只有三個欄位**:

```
channel: telegram
target:  -1001234567890
viewed:  2026-08-18 20:43:20
```

`viewed` 是上一輪檢討看到哪裡為止,留空就是看全部;每檢討完一輪就推到當下,
下次只看新的。**`.tap` 絕不進版控**——頻道位置是敏感資訊。

要讓每一則都帶著「下場」,plugin 設定裡的 `debug` 要是 `true`;
沒開也還是有逐字稿,只是不知道哪些被攔到了。

## 測試

```bash
npm test
```

不需要 gateway、不需要網路,也不依賴任何外部 script——測試自己把 fixture
寫進暫存目錄。

## 開發

Plain ESM JavaScript,沒有 build step。`--link` 安裝後改完
`src/index.js` 重啟 gateway 就生效。

AI model used: Claude Opus 5.0
tools

Comments

Sign in to leave a comment

Loading comments...