--- summary: "OpenClaw 如何发现、门控、加载和注册插件,以及插件缓存保存什么" read_when: - "调试插件加载顺序或注册表状态时" - "您需要知道 OpenClaw 何时读取清单而不是加载插件运行时" - "您正在推理插件缓存生成及其保留的内容" title: "插件加载管道和注册表" sidebarTitle: "加载管道和注册表" --- 发现、安全门、清单优先元数据、插件缓存边界以及核心读取的注册表。 [插件架构内部](/plugins/architecture-internals)指南的一部分。 ## 加载流水线 在启动时,OpenClaw 大致会执行以下步骤: 1. 发现候选插件根目录 2. 读取原生或兼容捆绑清单以及包元数据 3. 拒绝不安全的候选项 4. 规范化插件配置(`plugins.enabled`、`allow`、`deny`、`entries`、 `slots`、`load.paths`) 5. 为每个候选项决定是否启用 6. 加载已启用的原生模块:已构建的捆绑模块使用原生加载器; 第三方本地源码 TypeScript 使用紧急 Jiti 回退 7. 调用原生 `register(api)` 钩子,并将注册内容收集到插件注册表中 8. 将注册表暴露给commands/runtime表面 本机插件导入在链接插件的异步模块图之前同步评估每个请求的 SDK 条目。这使得 CommonJS 在并发通道启动期间捆绑 `require()` 相同的 SDK 条目,而不会看到未完成的 ESM 模块。未使用的 SDK 条目保持卸载状态。 捆绑的频道条目和频道元数据模块使用共享缓存模块加载器。它在评估之前准备 SDK 别名,并拥有本机到源的回退;通道适配器不会通过另一个加载器重试失败的评估。 安全门会在**运行时执行之前**运行。发现阶段会在以下情况下阻止候选项: - 其解析后的入口逃逸出了插件根目录 - 其路径(或其根目录)是全局可写的 - 对于非捆绑插件,其路径所有权与当前 uid(或 root)不匹配 在 Windows 上,插件根可以位于目录连接下方,包括 Node 版本管理器前缀。在打开条目之前,根别名由目录身份解析和验证;文件必须仍然保留在该根目录中。 全局可写的捆绑目录在门重新检查之前首先进行就地 `chmod` 修复尝试(npm/global 安装可以在 `0777` 处发送包目录);对于捆绑源完全跳过所有权检查。 当已知插件 id 时,被阻止的候选项在发出的诊断信息中仍会携带该 id(包括从一个在其他方面被拒绝的目录中的清单解析出的 id),因此引用该 id 的配置会看到一个与路径安全警告绑定的被阻止插件,而不是无关的“未知插件”错误。 ### 清单优先行为 清单是控制面的事实来源。OpenClaw 使用它来: - 识别插件 - 发现声明的 channels/skills/config 架构或捆绑功能 - 验证 `plugins.entries..config` - augment Control UI labels/placeholders - 显示 install/catalog 元数据 - 在不加载插件运行时的情况下保留廉价的激活和设置描述符 对于原生插件,运行时模块是数据面部分。它负责注册实际行为,例如钩子、工具、命令或提供者流程。 可选清单 `activation` 和 `setup` 块保留在控制平面上。它们是用于激活计划和设置发现的纯元数据描述符;它们不会替换运行时注册、`register(...)` 或 `setupEntry`。实时激活消费者在更广泛的注册表实现之前使用清单激活元数据来缩小插件加载范围: - CLI 加载会缩小到拥有所请求主命令的插件 - channel setup/plugin 分辨率缩小为拥有所请求通道 ID 的插件 - explicit 提供程序 setup/runtime 分辨率缩小为拥有所请求的提供程序 ID 的插件 - agent 运行时规划范围缩小为在 `activation.onAgentHarnesses` 中声明所选嵌入式harness运行时 ID 的插件 - startup 插件选择添加了 `activation.onConfigPaths` 条目存在并在配置中启用的插件 - Gateway 启动规划会对显式启动导入使用 `activation.onStartup`;没有启动元数据的插件只会通过更窄的激活触发器加载 激活规划器同时为现有调用方提供仅 ids 的 API,以及用于诊断的 plan API。计划条目会报告插件被选中的原因,将显式的 `activation.*` 提示与清单所有权回退区分开来: | 原因(来自 `activation.*` 提示) | 原因(来自清单所有权) | | ------------------------------------ | -------------------------------------------------------------------------------------------- | | `activation-agent-harness-hint` | — | | `activation-capability-hint` | — | | `activation-channel-hint` | `manifest-channel-owner` (`channels`) | | `activation-command-hint` | `manifest-cli-command-owner` (`cliCommands`), `manifest-command-alias` (`commandAliases`) | | `activation-provider-hint` | `manifest-provider-owner` (`providers`), `manifest-setup-provider-owner` (`setup.providers`) | | `activation-route-hint` | — | | —(hook 触发没有 hint 变体) | `manifest-hook-owner` (`hooks`), `manifest-tool-contract` (`contracts.tools`) | 这种原因拆分就是兼容边界:现有插件元数据继续可用,而新代码可以在不改变运行时加载语义的情况下检测更宽泛的提示或回退行为。 请求广泛的 `all` 范围的请求时运行时预加载仍然从配置、启动计划、配置的通道、插槽和自动启用规则(`src/plugins/effective-plugin-ids.ts` 中的 `resolveEffectivePluginIds`)派生出显式有效的插件 ID 集。如果该派生集为空,则 OpenClaw 会将范围保持为空,而不是扩大到每个可发现的插件。 对于仍需要安装时运行时挂钩的插件,安装程序发现更喜欢描述符拥有的 ID(例如 `setup.providers` 和 `setup.cliBackends`)来缩小候选插件范围,然后再回退到 `setup-api`。提供程序设置列出使用清单 `providerAuthChoices`、描述符派生的设置选项和安装目录元数据,而无需加载提供程序运行时。显式 `setup.requiresRuntime: false` 是仅描述符截止;省略 `requiresRuntime` 保留旧版 setup-api 后备兼容性。如果多个发现的插件声明相同的规范化安装提供程序或 CLI 后端 ID,则安装查找会拒绝不明确的所有者,而不是依赖发现顺序。当安装运行时执行时,注册表诊断会拒绝未声明的提供程序和 CLI 后端注册。 CLI 后端描述符还报告缺少运行时注册;提供者描述符可能仅保留元数据,而设置模块提供其他设置挂钩。 ### 插件缓存边界 一个 `PluginCache` 拥有保留一代的插件事实。 CLI 预检和启动逐渐填充相同的缓存;稍后的访问仅填充尚未获取的事实。其不可变的元数据快照结合了每个配置的代理工作区中已安装的索引、清单、所有者映射和可用的发现事实。禁用的插件保留在清单中,因此以后启用不需要发现。来自不同工作区源的冲突插件 ID 仍会被拒绝。 运行时读取器使用此 `PluginMetadataSnapshot`、派生的 `PluginLookUpTable` 或显式清单注册表。插件范围是内存中的投影;普通查找、帐户更改和运行工作区更改不会触发文件系统扫描、`stat`/`realpath` 新鲜度轮询、清单重读或哈希。插件生命周期操作在自己的缓存生成中准备新的元数据。帐户运行状况和身份验证状态不是不可变包清单的一部分。 网关模型准入使用已配置代理的工作区和插件清单,包括其发布的发现目录。在另一个文件夹中打开的会话会将该文件夹保留为其执行 cwd,而不会丢失已发现的模型。独立主机继续将插件发现范围限制在其请求的工作空间中。 设置提供程序检测从选择其选项的元数据快照加载提供程序。它的操作缓存仅拥有探针注册和插件实例,它们在探针稳定后退出,因此重复检测不会重新发现插件包。安装激活仍然准备新的元数据,因为它可以安装即将加载的插件。 显式安装或刷新会清除命令的可变发现缓存,因此其下一个元数据阶段会观察更新的清单。不可变的运行时世代保留其捕获的元数据。每个进程通常拥有一个网关。在一个流程中重叠独立网关是尽最大努力,而不是协调同一库存的所有者。 本机 SDK 别名解析保留同一代中每个导入文件的已解析文件系统路径、根成员资格和别名目标。别名注册或替换会清除这些结果,元数据失效也会清除这些结果;重复导入不会重复文件系统路径解析或包含检查。 相同的缓存生成准备安装索引范围查找、编译模型匹配模式、解析安装记录投影以及每个不可变索引一次的清单指纹。可变管理索引保持未缓存状态。查找方法和安装记录结果仍然由调用者拥有;支持和信任是根据当前操作的策略进行评估的,而不是存储在这些事实中。 在保留代之外,重用已加载的插件需要选择ID、起源、根、入口点和工件选择输入以达成一致。装载机记录source/build加载所有者上的选择和任何执行的设置条目。显式清单和发现源选择(包括承认的 sidecar)也参与加载程序缓存键。原始发现在主动重用之前还需要匹配负载身份,因为其明显的获胜者尚未确定。保留代仍然具有权威性,包括空选择。Source/build仅当加载器将视图解析为保留首选项下的相同执行根和条目时,视图才可以共享所有者。路径名或匹配的条目词干本身并不能建立所有权。运行时重用和加载共享现有的生命周期拥有的工件事实和最终执行步骤,包括执行的设置条目。发现和工件身份保留其选择的路径。 当未指定首选项时,有界加载所有者查找会保留所有者的工件策略。检查明确的偏好;准确的加载器请求应用完整的缓存标识和冷加载默认值。 完成的注册表会缓存在其原始请求及其解析的清单选择下。重用那些准备好的清单不会重复插件注册。这两个键共享现有的有界缓存,并在注册表退出或清除加载缓存时被删除。验证、完整注册和 CLI 元数据加载具有单独的缓存条目;验证模块无法满足以后的注册请求。 出站通道引导程序会记住所选插件缓存和元数据范围内的成功和不可用的发件人。库存更换或元数据失效允许新的尝试;同一范围内的重复交付可重用结果,而无需重试失败的注册。请求范围的通道所有者仍然优先于进程根引导结果。有效负载准备在其处理程序上携带所选发送者的指令策略,因此在解析和应用通道转换之前,一批会解析其插件一次。 提供程序查找首先使用显式调用者工作区,然后使用其元数据快照记录的工作区,包括显式共享根范围。只有没有工作区字段的缩小元数据视图才会继承活动工作区。注册表的现有加载上下文保留其工作区,因此来自另一个工作区的请求或活动注册表无法替换准备好的选择。 提供程序挂钩使用共享提供程序注册表选择。声明的提供者保留他们的名字,而提供者触发的帮助者仍然在他们旁边激活。所需的负载所有者和合格的挂钩接收器在该选择中单独捕获。声明的提供者所有者在重复使用之前需要匹配物理记录和提供者注册;仅激活帮助程序可能没有提供程序行,但需要成功完成的运行时注册通行证。仅设置阶段无法证明其运行时贡献是完整的。没有静态所有者的引用仅选择请求范围内的匹配运行时别名。加载的别名可以识别要重新加载的所有者,但只有所选注册表的当前注册才能确定别名接收者。仅加载的故障转移检查永远不会发现或激活插件。当它使用注册表拥有的元数据时,调用者的发现和策略输入必须与记录的指纹匹配。加载器一次捕获规范化的注册输入,包括配对的源配置;稍后的元数据更新会保留该记录。仅当这些输入匹配时,普通查找才会重用回调;保留下来的一代人仍然具有权威。模型引用解析从注册表或保留的请求范围中读取相同的声明所有者事实,因此失败的声明提供程序不能被另一个提供程序的钩子别名替换。 提供程序身份验证别名已标准化并使用快照建立索引。查找使用当前工作区信任配置在那些准备好的候选者中进行选择;它们不缓存信任决策或凭证。提供部分清单视图的调用者保持每次调用的最新投影,而不是共享可变元数据。 显式安装、更新、注册表刷新和医生操作使用相同缓存类型的隔离代,这些缓存类型是在其生命周期租用后获取的。他们可能会检查更改的文件并重建持久安装的索引。实时库存替换属于网关插件生命周期:`plugins.refresh` 准备并应用新一代,然后使用 `restartRequired: false` 返回其运行时收据,包括禁用被动配置重新加载时。独立注册表或医生修复本身并不证明网关应用了修复的文件;需要时使用 [ 插件 Reload](/cli/plugins#reload) 。 共享缓存拥有检查的文件内容、解析的包和清单数据、捆绑 MCP/LSP/settings 文件、插件技能路径、发现路径、安装索引投影、编译的模型策略、SDK 别名、工件位置和惰性模块导出。丢失的文件和工件也是事实:它们一直丢失,直到新一代。发现、注册表组装和索引散列重用相同的检查字节,而不是在每个阶段重新打开文件。 实际代码导入在首次执行之前保留其边界和文件身份检查。同意检查在等待批准后使用新的检查,因此更改的工件无法继承对旧功能的批准。插件缓存释放失败的加载,但 Node 在进程生命周期内保留失败的本机 ESM 评估;重新启动帐户无法修复该模块图。成功的进口是由消费者分享的。 文档和 Web 内容提取从每个请求的当前元数据范围中选择回调。共享配置对象不会使两个清单可以互换;插件缓存仍然重用它们的模块导出。公共工件适配器通过提供者选择和模块加载来承载显式环境,包括所选配置文件的捆绑发现策略。当选择提供清单所有者时,工件会从该所有者的根和条目解析,从而保留源覆盖和保留的模块实例。 捆绑的提供者策略查找在元数据缓存中保留其已解析的表面(包括缺失)。重复的模型参考标准化重复使用该表面,而无需再次解析候选工件。该备忘录遵循所选注册表的发布版本和捆绑目录选择,并为每个注册表提供单独的弱保留条目。当加载程序发布其身份时,完整的私有注册表将变得可缓存;注册和不完整的注册仍然不被缓存。新一代或显式元数据失效会再次解析表面,并且托管表面保留其实例的准入检查。 CLI 调用拥有一个跨配置读取、输出元数据、命令所有权、嵌套注册和操作的操作缓存。独立注册使用其调用者的活动生成。配置验证覆盖每个工作区;执行时使用原始选定的工作区快照,或在未证明工作区所有者时使用共享根。如果配置在操作期间发生变化,准备好的注册商将保留捕获的元数据。准备工作在 Commander 操作之前关闭,而其缓存范围持续到后期导入的操作完成。改变包文件需要重新操作;更改激活输入不会废弃兼容的包事实。 SDK 别名映射是根据第一个别名或变压器需求在其捕获的主机和权限范围下准备的。状态注册使用了完整运行时稍后采用的轻量外观;只有注册表代理授予存储访问权限。仅当实际写入开始时,配置读取才会导入写入器。 注册的服务、挂钩、工具、会话 MCP 覆盖、生成的技能链接发布和激活状态仍由运行时拥有。活动注册表固定其选择的工件绑定,因此源模块和构建模块无法分割其注册。本机 ESM 模块生命周期仍遵循 Node 的模块加载程序。清单派生的问题,例如“哪个插件拥有此提供程序?”使用元数据快照而不执行插件代码。持久安装的索引属于管理和启动;对于运行时读者来说,这不是新鲜感信号。 ## 注册表模型 加载的插件不会直接改变随机核心全局变量。它们注册到中央插件注册表(`src/plugins/registry-types.ts` 中的 `PluginRegistry`),该注册表跟踪插件记录(身份、源、起源、状态、诊断)以及每种功能的数组:工具、遗留钩子和类型化钩子、通道、提供程序、网关 RPC 处理程序、HTTP 路由、CLI 注册器、后台服务、插件拥有的命令以及数十个类型化的提供程序系列(语音、嵌入、image/video/music 生成、Web fetch/search、代理工具、会话操作等)。 核心功能随后会从该注册表中读取,而不是直接与插件模块交互。这使得加载方向保持单向: - 插件模块 -> 注册表注册 - 核心运行时 -> 注册表消费 这种分离对可维护性很重要。它意味着大多数核心表面只需要一个集成点:“读取注册表”,而不是“为每个插件模块做特殊处理”。