扩展系统 extensions
昨天(Day 11)讲的技能是"数据/说明书";今天讲的扩展是"代码"。extension(代码里叫 plugin)是可执行 TS 模块,能向宿主注入 provider / channel / tool / hook / 服务 / HTTP 路由。今天看它怎么被发现、加载、注册——这也为后两天(Day 13/14)讲"渠道其实就是一种扩展"打基础。
register(api))声明"我要用相机、通讯录"(注入 provider/channel/tool/hook)。而应用商店先审清单、后运行:先读 App 的说明书(openclaw.plugin.json)决定能不能装,确认没问题才真正运行它的代码。抓住"装 App + 先审后跑",今天就通了。extension = plugin
register(api),在里面调 api.registerProvider/registerChannel/registerTool/on(...) 把新能力插进 OpenClaw。技能扩"知识玩法",扩展扩"底层能力"——这是两者的根本分工。先厘清术语:目录叫 extensions/,但类型/清单/代码里都叫 plugin(OpenClawPlugin*、openclaw.plugin.json、src/plugins/)。本课"扩展"和"插件"是同一个东西。
OpenClawPluginApi(src/plugins/types.ts:366-409);面向插件作者的 SDK 入口是 src/plugin-sdk/(扩展 import "openclaw/plugin-sdk/<子路径>")。(src/extensionAPI.ts 只是 15 行的再导出壳,不是真 API。)扩展 vs 技能
| 技能 skill(Day 11) | 扩展 extension(今天) | |
|---|---|---|
| 形态 | SKILL.md(markdown 数据) | TS 模块(可执行代码) |
| 怎么生效 | 提示词 + 模型读文件照做 | 被 import 并调 register(api) 注入能力 |
| 能干什么 | 教模型用已有工具完成任务 | 注入新 provider/channel/tool/hook/服务 |
| 门槛 | 写 markdown,零代码 | 写 TS 代码 |
skills?: string[] 字段附带技能目录。扩展的结构
每个 extensions/<名>/ 含三样:
openclaw.plugin.json:清单(必需),含id+configSchema。package.json:含openclaw.extensions: ["./index.ts"]声明入口。index.ts:默认导出注册逻辑。
真实例子 extensions/ollama/(provider 型):
// openclaw.plugin.json
{ "id": "ollama", "providers": ["ollama"], "configSchema": { ... } }
register(api)
入口 index.ts 的 register(api) 是扩展往宿主"插能力"的地方。四类真实例子:
// Provider 型(ollama):注入新模型来源
register(api) { api.registerProvider({ id, label, auth, discovery, wizard, onModelSelected }); }
// Channel 型(telegram):注入新聊天渠道
register(api) { api.registerChannel({ plugin: telegramPlugin }); }
// Tool 型(llm-task):注入新工具
export default function register(api) { api.registerTool(createLlmTaskTool(api), { optional: true }); }
// Memory 型(memory-lancedb):注入工具 + 钩子 + 服务
register(api) {
api.registerTool({ name: "memory_recall", parameters, execute });
api.on("before_agent_start", autoRecall);
api.on("agent_end", autoCapture);
api.registerService(...);
}
api.registerX(...) 系列是扩展的"能力插槽":Provider/Channel/Tool/Hook/Service/HttpRoute/Cli/Command/ContextEngine。register 必须同步执行(加载器要求)。createApi 在 src/plugins/registry.ts:186,各 registerX 把东西推进 PluginRegistry。加载流水线
loadOpenClawPlugins()(loader.ts:517-897)五步:
- 发现
discoverOpenClawPlugins:扫 4 类 root(bundledextensions/、global<configDir>/extensions、workspace<ws>/.openclaw/extensions、config loadPaths)。 - 清单注册:只读
openclaw.plugin.json(不执行),按来源优先级 config > workspace > global > bundled(用户可覆盖内建)。 - 逐个加载:去重、解析 enable、memory 槽位互斥(只允许一个 memory 扩展生效)、要求 configSchema。
- import + register:用 jiti(
loader.ts:753)import TS 入口,校验配置,createApi()后调register(api)。 - 激活
activatePluginRegistry→initializeGlobalHookRunner让钩子生效。
extensions/ollama/ → 读它的 openclaw.plugin.json({"id":"ollama","providers":["ollama"]})→ 决定启用、无冲突 → jiti import index.ts → 调 register(api),其中 api.registerProvider({id:"ollama",...}) → 结果:助理的"可选模型来源"里多了一个本地 ollama,你在配置里就能选它。全程只有最后一步才真跑了 ollama 扩展的代码。生命周期钩子
扩展可用 api.on(hookName, handler, {priority}) 挂钩到 Agent 生命周期各点(PluginHookName,types.ts:424-448):
before_model_resolve // 换模型(Day 06)
before_prompt_build // 注入系统提示(Day 09,受策略门控)
before_agent_start / agent_end // 运行前后(memory 扩展的自动召回/捕获)
llm_input / llm_output // 模型输入输出
before_tool_call / after_tool_call // 工具调用前后(before 可 {block:true} 否决!)
message_received / message_sending / message_sent
session_start / session_end / gateway_start / gateway_stop
before_compaction / after_compaction / subagent_*
before_agent_start 自动检索相关记忆、agent_end 自动保存新记忆;安全扩展在 before_tool_call 拦截危险调用。这是典型的"事件钩子/中间件"架构,和你学过的 crewAI events、OpenHands 事件流同源。👶 小白:before_tool_call 那个 {block:true} 有啥特别?听起来就是普通回调。
👨🏫 老师:它不只是"旁听",而是能否决。举个出事场景:模型被一段恶意网页内容诱导,准备调 exec("rm -rf ~")。如果没有拦截点,工具就真跑了。有了 before_tool_call 钩子,安全扩展能在执行之前检查这次调用,返回 {block:true} 把它挡下来。大多数钩子是"通知型"(发生了就告诉你),而 before_* 系列是"守门型"(能改甚至能拦)——这正是把安全策略做成可插拔扩展的关键(Day 17 详讲)。
安全门控
扩展是"可信计算基"(装了就等于本地代码同权限,Day 17 会展开),但加载器仍有多道门控:
- 路径安全:
isUnsafePluginCandidate拒绝全局可写/越界目录(discovery.ts:250-272)。 - 注册冲突:拒绝重复 provider id、重叠 HTTP 路由。
- 提示注入门控:
before_prompt_build注入受allowPromptInjection策略约束(registry.ts:520-567)。 - memory 槽位互斥:多个 memory 扩展只放行一个。
今日小结 + 动手
🧠 今天你应该能回答
- extension 和 plugin 什么关系?
- 扩展和技能的四点区别?
- 扩展的三个文件 + register(api) 干什么?
- 加载流水线为什么"先读清单后执行代码"?
- 生命周期钩子举 3 个用途?before_tool_call 能做什么特别的事?
✋ 动手
cd /Users/bitmart/work/codes/github/openclaw
ls extensions/ | head -40
cat extensions/ollama/openclaw.plugin.json
grep -n 'register(api)\|registerProvider\|registerChannel' extensions/ollama/index.ts extensions/telegram/index.ts
grep -n 'PluginHookName' src/plugins/types.ts
ChannelPlugin 用"组合式 adapter"(不是基类继承)定义收/发/连接/归一化契约。