跳转至

加载管道和注册表

发现、安全门、清单优先元数据、插件缓存边界,以及核心读取的注册表。属于插件架构内部机制指南的一部分。

加载流水线

启动时,OpenClaw 大致执行以下操作:

  1. 发现候选插件根目录
  2. 读取原生或兼容包清单与包元数据
  3. 拒绝不安全的候选插件
  4. 规范化插件配置(plugins.enabled、allow、deny、entries、slots、load.paths)
  5. 为每个候选插件决定启用状态
  6. 加载已启用的原生模块:内置捆绑模块使用原生加载器;第三方本地 TypeScript 源码使用紧急 Jiti 回退
  7. 调用原生 register(api) 钩子并将注册项收集到插件注册表
  8. 将注册表暴露给命令/运行时表面

原生插件导入在链接插件的异步模块图之前,会同步求值每个被请求的 SDK 条目。这使得 CommonJS 捆绑包在并发通道启动期间可以 require() 同一个 SDK 条目,而不会看到未完成的 ESM 模块。未使用的 SDK 条目保持不加载状态。

捆绑的通道条目和通道元数据模块使用共享的缓存模块加载器。该加载器在求值前准备 SDK 别名,并负责原生到源码的回退;通道适配器不会通过另一个加载器重试失败的求值。

安全门在运行时执行之前运行。发现过程在以下情况下阻止候选插件:

  • 其解析后的入口逃逸出插件根目录
  • 其路径(或其根目录)是全局可写的
  • 对于非捆绑插件,路径所有权与当前 uid(或 root)不匹配

全局可写的捆绑目录会在安全门重新检查之前,先尝试就地 chmod 修复(npm/全局安装可能会以 0777 权限发布包目录);对于捆绑来源则完全跳过所有权检查。

被阻止的候选插件在已知插件 id 时(包括从本会被拒绝的目录内的清单解析出的 id),仍会在发出的诊断信息中携带该 id,因此引用该 id 的配置会看到与路径安全警告关联的被阻止插件,而不是无关的“未知插件”错误。

清单优先行为

清单是控制平面的权威来源。OpenClaw 使用它来:

  • 识别插件
  • 发现声明的通道/技能/配置模式或包能力
  • 验证 plugins.entries.<id>.config
  • 增强 Control UI 标签/占位符
  • 显示安装/目录元数据
  • 在不加载插件运行时的情况下保留轻量的激活和设置描述符

对于原生插件,运行时模块是数据平面部分。它注册实际行为,如钩子、工具、命令或提供者流程。

可选的清单 activation 和 setup 块保留在控制平面上。它们仅用于激活规划和设置发现的元数据描述符;它们不会替代运行时注册、register(...) 或 setupEntry。实时激活消费者使用清单激活元数据来在更广泛的注册表物化之前缩小插件加载范围:

  • CLI 加载缩小到拥有所请求主命令的插件
  • 通道设置/插件解析缩小到拥有所请求通道 id 的插件
  • 显式提供者设置/运行时解析缩小到拥有所请求提供者 id 的插件
  • 代理运行时规划缩小到在 activation.onAgentHarnesses 中声明所选嵌入式 harness 运行时 id 的插件
  • 启动插件选择会添加那些 activation.onConfigPaths 条目在配置中存在且已启用的插件
  • Gateway 启动规划使用 activation.onStartup 进行显式启动导入;没有启动元数据的插件仅通过更窄的激活触发器加载

激活规划器既为现有调用者暴露仅 id 的 API,也为诊断暴露计划 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 —
—(钩子触发器没有提示变体) manifest-hook-owner (hooks)、manifest-tool-contract (contracts.tools)

这种原因拆分是兼容性边界:现有插件元数据继续工作,而新代码可以检测宽泛提示或回退行为,而无需改变运行时加载语义。

请求时请求宽泛 all 作用域的运行时预加载仍然会从配置、启动规划、已配置通道、槽位和自动启用规则中推导出显式的有效插件 id 集(src/plugins/effective-plugin-ids.ts 中的 resolveEffectivePluginIds)。如果推导出的集合为空,OpenClaw 会保持作用域为空,而不是扩大到每个可发现的插件。

设置发现优先使用描述符拥有的 id(如 setup.providers 和 setup.cliBackends)来缩小候选插件范围,然后对于仍需要设置时运行时钩子的插件回退到 setup-api。提供者设置列表使用清单 providerAuthChoices、从描述符推导的设置选项以及安装目录元数据,而无需加载提供者运行时。显式的 setup.requiresRuntime: false 是仅描述符的截止条件;省略 requiresRuntime 时会保留旧的 setup-api 回退以保持兼容性。如果多个被发现的插件声称拥有同一个规范化设置提供者或 CLI 后端 id,设置查找会拒绝存在歧义的所有者,而不是依赖发现顺序。当设置运行时执行时,注册表诊断会拒绝未声明的提供者和 CLI 后端注册。CLI 后端描述符还会报告缺失的运行时注册;提供者描述符可以保持仅元数据,而设置模块贡献其他设置钩子。

插件缓存边界

一个 PluginCache 拥有一个保留代的插件事实。 CLI 预检和启动会逐步填充同一个缓存;后续访问仅填充尚未获取的事实。其不可变元数据快照组合了已安装索引、清单、所有者映射,以及来自每个已配置 agent 工作区的可用发现事实。已禁用的插件仍保留在清单中,因此后续启用无需发现。来自不同工作区来源的冲突插件 ID 仍会被拒绝。

运行时读取方使用这个 PluginMetadataSnapshot、派生的 PluginLookUpTable,或显式清单注册表。插件作用域是内存中的投影;常规查找、账户变更和运行工作区变更不会触发文件系统扫描、stat/realpath 新鲜度轮询、清单重读或哈希计算。插件生命周期操作会在其自身的缓存代中准备新的元数据。账户健康状况和认证状态不属于不可变软件包清单。

同一缓存代会为每个不可变索引准备一次已安装索引作用域查找、编译后的模型匹配模式、解析后的安装记录投影以及清单指纹。可变管理索引保持未缓存。查找方法和安装记录结果仍由调用方持有;启用和信任基于当前操作的策略进行评估,而不是存储在这些事实中。

在保留代之外,复用已加载插件要求所选 ID、来源、根、入口点和构件选择输入一致。加载器会在已加载所有者上记录来源/构建选择和任何已执行的设置入口。显式清单和发现来源选择(包括已接纳的 sidecar)也参与加载器缓存键。原始发现在活跃复用前还需要匹配的加载身份,因为其清单胜出者尚未确定。保留代保持权威,包括空选择。来源/构建视图只有在加载器在保留偏好下将它们解析为相同执行根和入口时,才可共享一个所有者。仅凭路径名称或匹配的入口词干不能确立所有权。运行时复用和加载共享现有由生命周期拥有的构件事实和最终执行步骤,包括已执行的设置入口。发现和构件身份保留其选定的路径。

有界的已加载所有者查找在未指定偏好时保留所有者的构件策略。会检查显式偏好;精确加载器请求应用完整缓存身份和冷加载默认值。

已完成的注册表会同时在其原始请求和已解析的清单选择下缓存。复用这些已准备的清单不会重复插件注册。两个键共享现有有界缓存,并在注册表退役或加载缓存清除时移除。验证、完整注册和 CLI 元数据加载具有独立的缓存条目;验证模块不能满足其后续注册请求。

出站通道引导会在所选插件缓存和元数据作用域内记住成功和不可用的发送器。清单替换或元数据失效允许新的尝试;同一作用域内的重复投递会复用结果,而不会重试失败的注册。请求作用域的通道所有者仍优先于进程根引导结果。载荷准备会在其处理器上携带所选发送器的指令策略,因此一个批次在解析和应用通道转换之前只解析一次其插件。

提供者查找首先使用显式调用方工作区,然后使用其元数据快照记录的工作区,包括显式共享根作用域。只有没有工作区字段的窄化元数据视图会继承活动工作区。注册表的现有加载上下文保留其工作区,因此来自另一工作区的请求或活动注册表无法替换已准备的选择。

提供者钩子共享规范的提供者注册表选择。已声明的提供者保留其名称,而提供者触发的辅助器仍会在其旁边激活。必需的加载所有者和符合条件的钩子接收器在该选择中分别捕获。已声明的提供者所有者在复用前需要匹配的物理记录和提供者注册;仅激活辅助器可能没有提供者行,但需要成功完成的运行时注册过程。仅设置过程无法证明其运行时贡献已完成。没有静态所有者的引用仅在请求作用域内选择其匹配的运行时别名。已加载别名可以识别要重新加载的所有者,但只有所选注册表的当前注册决定别名接收器。仅加载的故障转移检查永远不会发现或激活插件。当它使用注册表拥有的元数据时,调用方的发现和策略输入必须与记录的指纹匹配。加载器一次性捕获规范化注册输入,包括配对的来源配置;后续元数据更新会保留该记录。常规查找仅当这些输入匹配时复用回调;保留代保持权威。模型引用解析从注册表或保留请求作用域读取相同的已声明所有者事实,因此失败的已声明提供者不能被其他提供者的钩子别名替换。

提供者认证别名会随快照一起规范化并建立索引。查找使用当前工作区信任配置在这些已准备的候选项中选择;它们不缓存信任决策或凭据。提供部分清单视图的调用方保持每次调用时新的投影,而不是共享可变元数据。

显式安装、更新、注册表刷新和 doctor 操作使用同一缓存类型的隔离代,并在其生命周期租约之后获取。它们可以检查已更改的文件并重建持久化的已安装索引。实时清单替换属于 Gateway 插件生命周期:plugins.refresh 准备并应用新代,然后返回其运行时回执,其中 restartRequired: false,即使被动配置重载被禁用也是如此。独立的注册表或 doctor 修复本身不能证明 Gateway 已应用修复后的文件;必要时使用 plugin Reload。

共享缓存拥有已检查的文件内容、解析后的包和清单数据、捆绑的 MCP/LSP/设置文件、插件技能路径、发现路径、已安装索引投影、编译后的模型策略、SDK 别名、制品位置以及惰性模块导出。缺失的文件和制品也是事实:它们会保持缺失,直到新的代际。发现、注册表组装和索引哈希复用相同的已检查字节,而不是在每个阶段重新打开文件。

实际代码导入在首次执行前保留其边界和文件身份检查。同意检查在等待批准之后使用新的检查,因此已更改的制品无法继承针对旧能力的批准。插件缓存会释放失败的加载,但 Node 会在进程生命周期内保留失败的原生 ESM 求值;重启账户无法修复该模块图。成功的导入会在各消费者之间共享。

文档和网页内容提取在每次请求时从当前元数据作用域中选择回调。共享的配置对象并不会使两个清单可互换;插件缓存仍会复用它们的模块导出。公共制品适配器在提供程序选择和模块加载中都携带显式环境,包括所选配置文件的捆绑发现策略。当选择提供清单所有者时,制品从该所有者的根和入口解析,保留源覆盖和保留的模块实例。

捆绑提供程序策略查找会在元数据缓存中保留其已解析的表面,包括缺失。重复的模型引用规范化会复用该表面,而不会再次解析制品候选项。该记忆化条目遵循所选注册表的发布版本和捆绑目录选择;注册和未发布的注册表保持未缓存。新的代际或显式元数据失效会再次解析该表面,并且受管理的表面保留其实例的准入检查。

CLI 调用拥有一个操作缓存,覆盖配置读取、输出元数据、命令所有权、嵌套注册和操作。独立注册使用其调用方的活动代际。配置验证覆盖每个工作区;执行使用最初选定的工作区快照,或者在未证明工作区所有者时使用共享根。精确的配置/源身份和修订检查会隔离保留的注册器。准备在 Commander 操作之前关闭,而其缓存作用域会持续到操作完成,以支持延迟导入。更改的包文件需要新的操作;更改激活输入不会废弃兼容的包事实。SDK 别名映射会在首次别名或转换器需求下,在其捕获的主机和权限作用域中准备。状态注册使用一个轻量外观,完整运行时稍后会采用它;只有注册表代理授予存储访问权限。配置读取只在实际写入开始时导入写入器。

已注册的服务、钩子、工具、会话 MCP 覆盖、生成的技能链接发布和激活状态仍由运行时拥有。 活动注册表会固定其选定的制品绑定,因此源模块和构建模块无法拆分其注册。原生 ESM 模块生命周期仍遵循 Node 的模块加载器。清单派生的问题(例如“哪个插件拥有此提供程序?”)会使用元数据快照,而不会执行插件代码。持久化的已安装索引属于管理和启动;它不是运行时读取器的新鲜度信号。

注册表模型

已加载的插件不会直接修改随机的核心全局变量。它们注册到一个中央插件注册表(PluginRegistry 在 src/plugins/registry-types.ts 中),该注册表跟踪插件记录(身份、来源、起源、状态、诊断信息),以及每项能力的数组:工具、旧版钩子和类型化钩子、通道、提供程序、网关 RPC 处理程序、HTTP 路由、CLI 注册器、后台服务、插件拥有的命令,以及数十个其他类型化提供程序族(语音、嵌入、图像/视频/音乐生成、网页抓取/搜索、代理框架、会话操作等)。

核心功能随后从该注册表读取,而不是直接与插件模块通信。这使加载保持单向:

  • 插件模块 -> 注册表注册
  • 核心运行时 -> 注册表消费

这种分离对可维护性很重要。它意味着大多数核心表面只需要一个集成点:“读取注册表”,而不是“为每个插件模块做特殊处理”。

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 cl/openclaw