卷五 22|plugin 到底是什么,它不是哪一种扩展点的壳¶
导读¶
- 所属卷:卷五:外部扩展与多代理能力
- 卷内位置:22 / 24
- 上一篇:卷五 21|为什么前面这些扩展点加起来,还是不够
- 下一篇:卷五 23|为什么 plugins 最后会长成一层平台边界
第 21 篇已经先证明:很多扩展入口并存,不等于插件层已经成立。
第 22 篇要继续切开的,是 plugin 的对象身份:
plugin 到底是什么?为什么它既不是 hooks 的壳,不是 marketplace 安装包,也不是某一种扩展点的同义词?
所以这篇只做一件事:把 plugin 写成统一运行时对象,而不是再来一种大号扩展点。
这篇要回答的问题¶
第 21 篇已经先回答了“为什么前面已经有 skills / MCP / hooks,系统还需要 plugin”。
接下来要补的不是“plugin 很重要”,而是更容易被误解的下一刀:
plugin 到底是什么?为什么它既不是 hooks 的壳,不是 marketplace 安装包,也不是某一种扩展点的同义词?
如果这一步不回到运行时对象本身,文章就会很容易滑向三种轻飘的说法:
- plugin 就是扩展点总称
- plugin 就是 hooks 外面再包一层
- plugin 就是后来拿来 install / update / marketplace 的产品壳
这三种说法都抓到了一点边,但都没打中 Claude Code 里最硬的那个事实:
plugin 先是统一运行时对象,后面才轮到安装、治理和分发。
旧文与源码锚点¶
旧文素材锚点¶
docs/guidebook/volume-4/10-plugin-capability-surface.mddocs/guidebook/volume-4/12-plugin-attachment-points.mddocs/guidebook/volume-4/15-plugin-conclusion.md
源码锚点¶
/Users/haha/.openclaw.pre-migration/workspace/cc/src/types/plugin.ts/Users/haha/.openclaw.pre-migration/workspace/cc/src/utils/plugins/pluginLoader.ts/Users/haha/.openclaw.pre-migration/workspace/cc/src/utils/plugins/loadPluginHooks.ts/Users/haha/.openclaw.pre-migration/workspace/cc/src/utils/plugins/mcpPluginIntegration.ts/Users/haha/.openclaw.pre-migration/workspace/cc/src/cli/handlers/plugins.ts
说明:写作卡片里给的是
/Users/haha/.openclaw/workspace/cc/src/...。当前本地可核对实现实际位于/Users/haha/.openclaw.pre-migration/workspace/cc/src/...,本文按这套可读源码路径建立证据链。
主图:plugin 先是统一运行时对象¶
flowchart TD
A[LoadedPlugin] --> B[commands]
A --> C[agents]
A --> D[skills]
A --> E[hooks]
A --> F[output styles]
A --> G[MCP servers]
A --> H[LSP servers]
A --> I[settings]
J[pluginLoader] --> A
A --> K[runtime 分面接入]
A --> L[统一来源 / 启停 / 错误归因]
L --> M[之后才进入 install / update / marketplace]
先给结论¶
- 在 Claude Code 里,plugin 首先不是抽象分类词,而是
LoadedPlugin这种统一运行时对象。 - hooks、skills、MCP 只是 plugin 可以承载的组件面,不是 plugin 的全部身份。
- marketplace、install、update 当然也围着 plugin 转,但那是第二层:它们处理的是 plugin 的生命周期,不是 plugin 的第一定义。
主证据链¶
/Users/haha/.openclaw.pre-migration/workspace/cc/src/types/plugin.ts 里,LoadedPlugin 从定义层就同时承载 commands / agents / skills / output styles / hooks / MCP / LSP / settings → /Users/haha/.openclaw.pre-migration/workspace/cc/src/utils/plugins/pluginLoader.ts 的 createPluginFromPath(...) 会先造出这个统一对象,再把不同能力面一项项挂进去 → /Users/haha/.openclaw.pre-migration/workspace/cc/src/utils/plugins/loadPluginHooks.ts、.../mcpPluginIntegration.ts、commands / agents / output styles 相关加载链又分别从同一个 plugin 对象里拿各自那一面接进 runtime → 所以 plugin 不是某一种扩展点的壳,而是统一运行时宿主 / 统一能力承载对象;安装、市场、启停这些只是围绕这个对象后来长出来的管理层。
第一部分:先把 plugin 定义对——它是统一运行时对象,不是抽象大类¶
这篇最该先抓住的,不是“plugin 比别的更高级”,而是先把一个很常见的直觉纠正过来。
如果只看表面,plugin 很容易被理解成一个大号扩展包:
- hooks 多一点,外面包一层,叫 plugin
- skills、MCP、commands 放在一起,统称 plugin
- 后面能 install / update / marketplace,于是把它当成产品壳
但 Claude Code 的实现不是这样站位的。
它不是先把 hooks、skills、MCP 各自摆在那里,再统称成“插件世界”;它是先造出一个 LoadedPlugin 对象,再把这些不同能力面一项项挂进去。
也就是说,系统先认的是这个对象,后面才认这个对象里装了哪些东西。
plugin.ts 里的 LoadedPlugin 明确带着:
namemanifestpathsourcerepositoryenabledcommandsPath / commandsPathsagentsPath / agentsPathsskillsPath / skillsPathsoutputStylesPath / outputStylesPathshooksConfigmcpServerslspServerssettings
这串字段的意思非常直接:
系统不是把 hooks、skills、MCP 各自散放在外面,再在概念上叫它们“插件世界”;系统是先收出一个统一 plugin 对象,再让这些能力面寄宿在这个对象上。
所以这篇如果要给一句最稳的定义,我会写成:
plugin 是 Claude Code 里统一承载多种扩展能力面的运行时对象。
这里最关键的词不是“扩展”,而是:
- 统一
- 运行时对象
- 承载
因为这三个词刚好把常见误解挡掉了:系统先认对象,再认组件面。
它不是抽象总称¶
如果它只是总称,就不需要 LoadedPlugin 这种具体结构。
它不是某一类能力本身¶
如果它就是 hooks 或 skills,本体字段就不会同时把这么多组件面并列挂出来。
它不是只给分发系统看的包名¶
如果它的第一身份只是安装包,它的核心类型就会先围着版本、来源、安装位置转,而不是先围着 runtime 能力组成转。
第二部分:为什么说 plugin 不是 hooks 的壳¶
这是最容易滑进去的误解。
因为 hooks 在前面几篇里已经讲得很重,而 plugin 又确实能带 hooks,所以直觉上很容易把 plugin 理解成:
- hooks 的打包壳
- hooks 的管理层
- hooks 的外包装
但源码其实明明白白在反对这个理解。
第一条证据:hooks 在 LoadedPlugin 里只是一个字段¶
LoadedPlugin 里是 hooksConfig?: HooksSettings。
这句话表面上很普通,真正重要的是它背后的主谓关系:
- 主语是
LoadedPlugin - hooks 是它里面的一面配置
也就是说,Claude Code 不是先定义“hook plugin”,再围着 hook 加别的东西;而是先定义 plugin,再允许 plugin 其中一面是 hooks。
第二条证据:hook 真正进入 runtime 时,是从 plugin 对象里被抽出来注册¶
loadPluginHooks.ts 的逻辑很清楚:
- 先
loadAllPluginsCacheOnly()拿到所有 enabled plugins - 遍历每个 plugin 的
hooksConfig - 把它转换成带
pluginRoot / pluginName / pluginId的 matcher - 再统一注册进 hooks runtime
这个流程说明两件事:
1. hook 有自己的运行时语义¶
它是事件点上的 matcher 和 hook 列表,关心的是 PreToolUse、PostToolUse、SessionStart、Stop 这些 runtime 接缝。
2. plugin 不是 hook 本身,而是 hook 的宿主与归属边界¶
因为注册前,系统总是先问:
- 这个 hook 属于哪个 plugin
- 这个 plugin 是否 enabled
- 它的根目录是什么
- 它的 pluginId 是什么
所以这里最准确的说法不是“plugin 是 hooks 的壳”,而是:
hook 是 plugin 能携带的一种 runtime 接缝能力;plugin 则是把这类能力连同其它能力一起收进系统的正式对象。
换句话说:
- hook 回答的是:在哪个运行时接缝上介入。
- plugin 回答的是:这些介入能力属于哪个统一扩展对象。
这不是一回事。
第三部分:为什么说 plugin 不是 marketplace 安装包¶
第二个很常见的误解,是把 plugin 直接等同于安装和分发侧的对象。
这种误解也不是完全没来由。因为 plugins.ts 里确实已经有:
plugin listplugin installplugin uninstallplugin enableplugin disableplugin updateplugin marketplace *
但如果因此反过来把 plugin 定义成“安装包”,就把主次关系写反了。
为什么说主次关系反了¶
因为在实际代码顺序里,先出现的是统一运行时对象,后出现的是围绕它的管理动作。
更直一点说:
- 没有
LoadedPlugin,CLI 也就没有一个稳定对象可列举、启停、安装、卸载 - 正是因为系统先把 plugin 定义成正式对象,后面才值得长出 install / update / marketplace 这一整套生命周期
所以 marketplace 安装包只是 plugin 的进入系统方式之一,不是 plugin 的第一定义。
pluginLoader.ts 也在帮我们纠偏¶
pluginLoader.ts 文件头会讲 discovery sources,会讲 marketplace-based plugins、session-only plugins、builtin plugins。
这其实恰好说明:
source 是 plugin 的来源维度,不是 plugin 的本体定义。
同一个 plugin 对象可以来自:
- builtin
- marketplace
- inline / session-only
来源不同,不会让它突然从“plugin”变成别的东西;它进入系统后,仍然要落到同样的 LoadedPlugin 结构上。
所以这篇只需要轻轻点住一句:
plugin 可以被安装、被更新、被列举、被放进 marketplace,但这些都发生在“它已经是统一运行时对象”之后。
更完整的分发、治理、平台边界,留给第 23 篇讲就够了。
第四部分:为什么说 plugin 也不是某一种扩展点的同义词¶
到这里还要再往前走半步。
即使读者不把 plugin 误会成 hooks 壳,也可能会退回到另一个宽一点的误解:
- plugin 就是一种扩展点
- 只是这种扩展点比较大
- 比 skills、MCP、hooks 都大一点
这个说法还是不够准。
因为它会让人误以为 plugin 和 skills / hooks / MCP 只是并列同类,只是粒度不同。
但 LoadedPlugin 的结构已经告诉我们不是这样。
skills、hooks、MCP 是不同能力面¶
它们各自回答的问题不同:
- skills 更像方法组织与可调用经验单元
- hooks 更像 runtime 接缝上的观察、拦截、注入与改写
- MCP 更像外部能力源 / 外部 server 的接入面
plugin 不是再来一种“第四扩展点”¶
plugin 做的不是再提供一条新接入面,而是把前面这些不同接入面收进同一个运行时对象里。
这点可以从 createPluginFromPath(...) 看得很清楚。
它的做法不是:
- 发现一个 hooks 扩展点
- 再发现一个 skills 扩展点
- 再发现一个 MCP 扩展点
- 大家彼此平行
它的做法是:
- 先创建一个
plugin: LoadedPlugin - 然后为它补上
commandsPath - 补上
agentsPath - 补上
skillsPath - 补上
outputStylesPath - 继续补
hooksConfig - 继续补
mcpServers - 继续补
lspServers - 继续补
settings
这就说明:
plugin 不是“其中一种扩展点”,而是扩展点们共同寄宿的统一宿主。
所以第 23 篇真正要切开的层级关系,不是“谁更大”,而是:
- skills / hooks / MCP 是能力面
- plugin 是统一运行时对象
前者是内容面,后者是宿主面。
第五部分:plugin 统一的是对象边界,不是能力语义¶
这句是这篇最后必须收住的判断。
如果 Claude Code 真把 plugin 设计成“超级扩展点”,那最自然的做法应该是把所有东西抹平成一种注册项。
但实际代码不是这样走的。
它反而很刻意地保留了差异:
- hooks 通过
loadPluginHooks.ts注册进 runtime hooks - MCP 通过
mcpPluginIntegration.ts读取和合并 server 配置 - commands / skills 走各自的发现与枚举链
- agents 走 agent definition 装载链
- output styles 走样式收集链
也就是说,Claude Code 没有把所有能力语义压成一种统一协议;它只是在更高一层把它们收进同一个 plugin 对象。
所以 plugin 统一的不是:
- 方法语义
- 接缝语义
- 外部能力源语义
plugin 统一的是:
- 来源归属
- 启停边界
- 运行时宿主
- 错误归因
- 组件并置关系
这句话最好记成:
plugin 统一的是对象边界,不是能力语义。
也正因为如此,它才既不会把 hooks 吞掉,也不会把 skills、MCP 改名以后重讲一遍。
这篇的最终回答¶
现在可以把开头的问题压成一个读者真正能带走的版本:
plugin 在 Claude Code 里,首先是
LoadedPlugin这种统一运行时对象:它能同时承载 commands、agents、skills、hooks、output styles、MCP、LSP 和 settings。hooks、skills、MCP 只是它承载的不同组件面,不是它的全部身份。也因此,plugin 不是 hooks 的壳,不是 marketplace 安装包,也不是某一种扩展点的同义词;它是系统用来接住多能力面的统一宿主与统一边界。
如果再压缩成一句最值得记住的话,就是:
plugin 先是统一运行时对象,后面才轮到安装、治理和分发。
这就是第 22 篇该做完的事。
第 23 篇再继续回答:为什么围绕这个统一对象,系统最后还会长出 loader、schema、policy、install、marketplace,直到 plugin 变成一层平台边界。