组件开发 · 第 3 篇 / 共 3 篇 —— 权限

定义组件权限

这一篇主要给接入第三方消息通道的组件用。别人的消息从你写的接口推进来,你就得回答两个问题:这条消息的发送者能不能调本组件的命令(TOOL_POLICY),这次推送触发的任务能用哪些工具(TRIGGER_TOOL_POLICY)。

前置:组件能接收外部消息、能把消息交给 Agent 处理。纯本地工具类组件不对外接消息,基本用不到这一篇。

1. 命令级:谁能调用本组件的命令

在组件类里声明 TOOL_POLICY,键是裸命令名(不带组件名前缀),值决定谁能用。

class NapcatComponent(BaseCell):
    # 键是裸命令名,不带组件名前缀
    TOOL_POLICY = {
        "reset_config": "deny",                                      # 所有人禁用
        "add_user": "admin",                                         # 仅管理员
        "shutdown": {"mode": "blacklist", "users": ["12345"]},       # 名单内用户禁用
        "send_private": {"mode": "whitelist", "users": ["10001"]},   # 仅名单内用户可用
    }
    TOOL_POLICY_DEFAULT = "allow"   # 未声明的命令默认放行,改成 "deny" 则未声明的全禁

五种取值

值效果
"allow"放行(默认)
"deny"谁都不许用
"admin"仅管理员可用
{"mode": "whitelist", "users": [...]}仅名单内用户可用
{"mode": "blacklist", "users": [...]}名单内用户禁用,其余可用

TOOL_POLICY_DEFAULT 控制未声明命令的默认行为:默认是 allow;改成 deny 后,只有显式放行的命令能调,适合高敏感组件。

2. 管理员怎么判定

BaseCell 已有默认实现:看 platform_context 里的 tool_allowed,没有则看 is_admin。QQ 通道会自动传 tool_allowed,所以大多数情况不用写代码。需要接自己的管理员名单时,重写 is_policy_admin。

# 默认实现(BaseCell 自带)
# tool_allowed 优先,其次 is_admin,都没有则视为非管理员

# 接自己的管理员名单时重写
def is_policy_admin(self, platform_context=None) -> bool:
    user_id = str((platform_context or {}).get("user_id") or "")
    return user_id in {"10001", "10002"}   # 你的管理员 QQ 号

判定内部报错会被当作非管理员处理,所以这个方法里不要抛异常,自己兜底返回 False。

3. 完整例子:只让管理员维护城市,所有人查天气

add_city / remove_city 需要管理员,query 未声明 → 走默认放行。

class WeatherBot(BaseCell):
    TOOL_POLICY = {
        "add_city": "admin",       # 加城市要管理员
        "remove_city": "admin",    # 删城市要管理员
        # query 未声明 → 默认所有人可用
    }

    def is_policy_admin(self, platform_context=None) -> bool:
        user_id = str((platform_context or {}).get("user_id") or "")
        return user_id in {"10001", "10002"}

    def _cmd_query(self, city: str = "", **kwargs) -> Dict[str, Any]:
        return {"success": True, "weather": f"{city} 晴 25℃"}

    def _cmd_add_city(self, city: str = "", **kwargs) -> Dict[str, Any]:
        ...

用户发「查北京天气」→ 正常执行;发「添加上海」→ Agent 调 add_city 被拦,工具直接返回错误,Agent 据此告知用户无权限。

# Agent 调用 add_city 时被拦,工具返回:
{
  "success": False,
  "error": "命令 add_city 仅管理员可用",
  "_source": "component:weather_bot"
}
# Agent 看到这个错误会向用户说明无权限,而不是重试

4. 任务级:本组件触发的任务能用哪些工具

TOOL_POLICY 只管自己组件内的命令。想让「经本组件触发的任务」禁用其他工具(比如 shell、sub_agent),声明 TRIGGER_TOOL_POLICY。支持工具名,也支持 工具名.命令名 的命令级精确拦截。

class NapcatComponent(BaseCell):
    # 经本组件转发的消息,Agent 不许用这些工具
    TRIGGER_TOOL_POLICY = {
        "mode": "blacklist",
        "tools": ["shell", "sub_agent", "qq_files.download"],
    }

效果:QQ 用户发消息 → 组件转发触发 Agent → 调 shell 被拦,调 qq_files.download 被拦,但 qq_files 的其他命令不受影响。触发请求的 source 必须等于组件的 cell_name,声明才会生效。

class MonitorComponent(BaseCell):
    # 告警触发的任务只许用这两个工具
    TRIGGER_TOOL_POLICY = {
        "mode": "whitelist",
        "tools": ["monitor", "napcat.send_msg"],
    }

    def _on_alert(self, alert):
        """告警回调:触发 Agent 处理,任务自动受限"""
        httpx.post(f"{self._base_url}/api/component/event", json={
            "session_id": "monitor:alerts",
            "message": f"CPU 超阈值: {alert}",
            "source": self.cell_name,     # ← 按这个字段匹配上面的声明
            "platform_context": {
                "platform": "monitor",
                "user_id": alert.user_id,
            },
        })

5. 触发时临时传权限

不写代码声明,直接在 /api/component/event 的 platform_context 里传。

传法效果
"tool_allowed": false启用系统默认黑名单(shell、file、component、sub_agent 等约 20 项全禁),适合转发普通用户消息
tool_policy blacklist自定义黑名单,与默认黑名单合并(只能加不能减)
tool_policy whitelist只放行列出的工具,其余全禁(默认黑名单不参与)
POST /api/component/event
{
  "session_id": "qq:12345:group",
  "message": "帮我处理下这个",
  "source": "napcat",
  "platform_context": {
    "platform": "qq",
    "user_id": "12345",
    "tool_allowed": false
  }
}

转发示例:普通用户只给 qq_files,管理员不带策略字段 → 完全不限制。

def _trigger_agent(self, message: str, user_id: str):
    is_admin = user_id in self._admins
    ctx = {"platform": "qq", "user_id": user_id, "message_type": "group"}
    if not is_admin:
        ctx["tool_policy"] = {"mode": "whitelist", "tools": ["qq_files", "napcat.send_msg"]}
    # 管理员不带任何策略字段 → 不限制

    httpx.post(f"{self._base_url}/api/component/event", json={
        "session_id": f"qq:{user_id}",
        "message": message,
        "source": self.cell_name,
        "platform_context": ctx,
    })

优先级:请求里显式传了 tool_policy 时,以调用方为准,组件声明被覆盖。没声明 TRIGGER_TOOL_POLICY 的组件,行为完全不变。

决策速查

想要的效果写在哪
本组件某命令谁都不许用TOOL_POLICY = {"xxx": "deny"}
本组件某命令仅管理员"xxx": "admin"
本组件某命令仅指定用户"xxx": {"mode": "whitelist", "users": [...]}
经本组件触发的任务禁用其他工具TRIGGER_TOOL_POLICY blacklist
经本组件触发的任务只许用几个工具TRIGGER_TOOL_POLICY whitelist
触发时临时定权限platform_context + tool_allowed / tool_policy

常见坑

  1. 键名格式不同:TOOL_POLICY 的键是裸命令名(add_user);TRIGGER_TOOL_POLICY 的键是工具名或 工具名.命令名(qq_files.download)。写错不报错,只是不生效。
  2. 白名单会绕过默认黑名单:whitelist 里写 shell 就能放行 shell,只对可信调用方用白名单。
  3. 黑名单只能加不能减:blacklist 模式自动与系统默认黑名单合并,想放行默认黑名单里的工具只能改用 whitelist。
  4. 命令级拦截依赖 command 参数:qq_files.download 这种键只在调用时显式传了 command 才命中;没传时按裸工具名匹配。
  5. WebUI 内部调用不传 user_id:whitelist 模式的命令在 WebUI 里也会被拦;blacklist 模式不传 user_id 则放行。
Security

权限拦截是「事后拒绝」而不是「不出现在工具列表」:被禁的工具仍然对 LLM 可见,调用时返回 error。想让模型一开始就不尝试,靠提示词约束,不要指望权限表。