codex-windows-bot 第一版插件管理只做 action bundle 的开关、分类、健康检查和能力目录过滤,不急着动态加载代码。
windows-bot-memory/plugin-manager
一提到插件系统,很容易想到动态加载代码:放一个插件目录,扫描入口文件,按名称 import,然后让插件自己注册命令。
这对很多桌面软件是自然设计,但对一个能从聊天入口触发本机动作的本地助手来说,第一版插件系统最好别这么做。
codex-windows-bot 的插件管理后来走了一个更保守的方向:插件不是新的执行器,也不加载任意代码。插件只是覆盖在 action registry 上的一层 action bundle。
也就是说,插件负责分组、开关、展示和健康检查;执行动作的还是原来的动作注册表、风险模型、确认门和固定 handler。
插件先做能力包,不做代码包
一个插件可以长这样:
{ "id": "projects", "name": "Project Registry", "category": "projects", "enabled": true, "description": "registered project context and safe checks", "actions": [ "projects.list", "projects.brief", "projects.run_check" ], "friendly_commands": ["/project", "/check"], "parameters": { "default_mode": "quick" }}这里最重要的一点是:actions 只能引用已经在动作注册表里存在的 action id。
插件自己不提供 handler,不带脚本路径,不接收任意命令,也不能改写风险等级。它只是说:“这些已登记动作属于同一个能力包,可以一起展示,也可以一起关闭。”
这样一来,第一版插件系统更接近能力治理层,暂时不碰扩展运行时。
开关要真的拦住执行
如果插件只是 UI 分类,那它的价值有限。
更有用的做法是:执行动作前,除了检查 action 自己是否启用,还要检查它所属插件是否启用。
执行路径可以简化成:
收到动作请求-> 读取动作注册表-> 校验私聊和调用边界-> 查动作风险、确认、参数、handler-> 查插件归属-> 插件关闭则拒绝执行-> 调用固定 handler这样就能把一组能力作为整体收起来。比如某天不想让自然语言任务代理继续接收新任务,可以关掉 task-agent 插件;底层动作仍在注册表里,但执行入口会被插件层挡住。
这比逐个改几十个 action 的 enabled 更稳,也更容易回滚。
core 不能被随手关掉
插件系统里通常需要一个基础能力包。
它应该包含健康检查、能力目录、插件目录、路由状态之类的基础动作。这个包不能像普通插件一样被随手关闭,否则系统会出现一个很尴尬的状态:能力被关掉了,但你连“现在关了什么”都查不到。
所以本地维护命令应该拒绝关闭 core。
这个限制主要是为了保住诊断通道。外层能力怎么开关,至少还得看得到健康状态、能力目录和插件状态。
参数包只能放非敏感默认设置
插件参数很容易失控。
一开始只是想放 default_limit、retention_days、default_mode,后来就有人想把 API key、聊天 id、脚本路径、环境变量名、URL、glob 或命令也塞进去。
这些都不该进插件参数。
插件参数适合放的是:
- 默认显示条数。
- 默认模式。
- 保留天数。
- 枚举型策略。
- 友好命令提示。
不适合放的是:
- 凭据类内容。
- 用户或群聊标识。
- 任意路径。
- 任意 URL。
- shell 命令。
- handler 名称。
- 风险等级覆盖。
- 确认门绕过选项。
插件层如果能改 handler、改风险、改确认,就不再是覆盖层,而是在偷偷变成第二套动作系统。
聊天侧先只读
插件管理最诱人的功能,是从聊天里开关插件。
但第一版最好别做。
更稳的顺序是:
plugins.listplugins.describeplugins.statusplugins.health也就是聊天侧只能看插件列表、描述、状态和健康结果。真正的 enable/disable 留在本机维护命令里,而且默认 dry-run,必须显式 apply 才写入配置。
这样做少了些“远程管理”的爽感,但避免了一类高风险误操作:一句聊天消息把整组本地能力打开,或把诊断能力关掉。
等插件注册表稳定一段时间后,再考虑确认门控的远程开关动作也不迟。
能力目录要能按插件过滤
插件层还有一个很实际的用途:让能力目录变得可读。
动作多了以后,一个平铺的 action list 很难看。用户更关心的是:
- 现在 task-agent 能做什么。
- memory 插件是不是只读。
- desktop-observe 有没有暴露点击或输入。
- apps 插件是否允许启动应用。
- reports 插件到底会不会读取正文。
所以 capabilities.list 应该支持按 plugin 过滤。它返回的是被脱敏过的能力目录:action id、分类、风险、是否启用、是否需要确认、属于哪个插件、简短说明。
它不该返回 handler、真实路径、内部策略、输出目录、确认码、配置细节或敏感匹配规则。
能力目录面向聊天入口,用来确认“我当前能安全调用什么”;维护者要看的完整内部状态不该从这里吐出来。
health 要检查引用关系
插件注册表有一个常见坏法:引用不存在的 action。
如果插件里写了一个已经删掉或改名的 action id,展示层可能还以为能力存在,执行时才发现坏掉。更糟的是,如果某个 action 没有插件归属,能力目录和开关行为就会变得不可预测。
所以插件 health 至少要检查:
- 每个插件 id 格式稳定。
- core 存在且启用。
- 每个插件只引用已注册 action。
- 同一个 action 不被多个插件重复声明,除非明确支持多归属。
- 插件参数不含敏感字段、路径、命令或 URL。
- 插件开关会影响 action 执行。
- 插件状态输出不暴露内部路径和敏感规则。
这类检查不会让系统更炫,但能让插件层长期不变形。
插件层别绕过原来的风险模型
最重要的原则是:插件层不提升权限。
如果一个 action 原本是只读,插件不能让它变成写入。如果一个 action 原本需要确认,插件不能因为自己启用了就绕过确认。如果一个 action 原本被禁用,插件也不能直接把它变成可执行。
插件层只做额外限制,不做额外授权。
这句话很简单,但能挡住很多后续设计漂移。
现在我的判断
本地助手的插件系统不一定要从动态加载开始。
第一版更值得做的是:
- 把已登记动作按能力包分组。
- 用插件开关控制一组动作是否可执行。
- 用只读插件视图给聊天侧展示能力状态。
- 用本地 dry-run/apply 管住配置写入。
- 用 health 检查插件和 action 的引用关系。
- 禁止插件参数携带凭据、路径、命令、URL 和权限覆盖。
这套设计看起来没有“插件市场”刺激,但更适合远程触发本机动作的场景。
我现在更关心的是,每项能力能不能说清楚:属于哪个包、风险是什么、要不要确认、为什么此刻能执行、关掉后会影响哪些动作。
这些问题能稳定回答,插件层才算真的帮助手变得可维护。
继续读