跳转至

架构

这是 OpenClaw 插件系统的深度架构参考。对于实用指南,请从以下专题页面之一开始。

安装和使用插件

添加、启用和排查插件问题的最终用户指南。

构建插件

包含最小可用清单的首个插件教程。

频道插件

构建消息频道插件。

提供商插件

构建模型提供商插件。

SDK 概览

导入映射和注册 API 参考。

公共能力模型

能力是 OpenClaw 内部的公共原生插件模型。原生插件可以注册一种或多种能力类型:

能力 注册方法 示例插件
文本推理 api.registerProvider(...) anthropic、openai
CLI 推理后端 api.registerCliBackend(...) anthropic、openai
嵌入 api.registerEmbeddingProvider(...) 提供商拥有的向量插件
语音 api.registerSpeechProvider(...) elevenlabs、microsoft
实时转录 api.registerRealtimeTranscriptionProvider(...) openai
实时语音 api.registerRealtimeVoiceProvider(...) google、openai
媒体理解 api.registerMediaUnderstandingProvider(...) google、openai
转录来源 api.registerTranscriptSourceProvider(...) discord、google-meet、teams-meetings、zoom-meetings
图像生成 api.registerImageGenerationProvider(...) fal、google、openai
音乐生成 api.registerMusicGenerationProvider(...) fal、google、minimax
视频生成 api.registerVideoGenerationProvider(...) fal、google、qwen
网页抓取 api.registerWebFetchProvider(...) firecrawl
网页搜索 api.registerWebSearchProvider(...) brave、firecrawl、google
频道 / 消息 api.registerChannel(...) matrix、msteams
网关发现 api.registerGatewayDiscoveryService(...) bonjour
迁移 api.registerMigrationProvider(...) migrate-claude、migrate-hermes

Note

仅注册钩子的插件是仅钩子插件。具有工具、命令、后台服务或路由但没有能力的插件是非能力插件。这两种模式仍受支持;网关发现是上面列出的显式能力。

外部兼容性立场

能力模型已落地到核心,并且目前由捆绑/原生插件使用,但外部插件兼容性仍需要比“已导出,因此已冻结”更严格的门槛。

插件情况 指导
现有外部插件 保持基于钩子的集成可用;这是兼容性基线。
新的捆绑/原生插件 优先使用显式能力注册,而不是供应商特定的内部访问或新的仅钩子设计。
采用能力注册的外部插件 允许,但除非文档标记为稳定,否则应将能力特定的辅助接口视为演进中。

能力注册是预期方向。旧版钩子仍然是外部插件在过渡期间最安全的无破坏路径。导出的辅助子路径并不都同等重要——优先选择狭窄的文档化契约,而不是偶然的辅助导出。

插件形态

OpenClaw 会根据插件的实际注册行为(而不仅仅是静态元数据)将每个已加载插件归类为一种形态:

plain-capability

恰好注册一种能力类型(例如像 arcee 或 chutes 这样的仅提供商插件)。

hybrid-capability

注册多种能力类型(例如 openai 拥有文本推理、语音、媒体理解和图像生成)。

hook-only

仅注册钩子(类型化或自定义),没有能力、工具、命令或服务。

non-capability

注册工具、命令、服务或路由,但没有能力。

使用 openclaw plugins inspect <id> 查看插件的形态和能力分解。有关详细信息,请参阅 CLI 参考。

兼容性信号

openclaw doctor、openclaw plugins inspect <id>、openclaw status --all 和 openclaw plugins doctor 会显示这些兼容性通知:

信号 含义
信号 含义
config valid 配置解析正常,且插件可解析
hook-only (info) 插件仅注册 hooks;这是受支持的路径,但尚未迁移到能力注册
deprecated memory-embedding API (warn) 非内置插件使用了旧的 memory 专用 embedding provider API,而不是 registerEmbeddingProvider
hard error 配置无效或插件加载失败

目前,这些建议/警告信号都不会导致你的插件失效。这些信号也会出现在 openclaw status --all 和 openclaw plugins doctor 中。

架构概览

OpenClaw 的插件系统包含四个层次:

1. 清单 + 发现

OpenClaw 会从已配置路径、工作区根目录、全局插件根目录和内置插件中查找候选插件。发现过程会优先读取原生 openclaw.plugin.json 清单以及受支持的 bundle 清单。

2. 启用 + 校验

核心会决定已发现的插件是启用、禁用、阻止,还是被选中用于独占槽位(例如 memory)。

3. 运行时加载

原生 OpenClaw 插件会在进程内加载,并将能力注册到中央注册表。托管实例通过 Node 加载 JavaScript,并在需要时编译 TypeScript 源码。兼容 bundle 会被规范化为注册表记录,而不会导入运行时代码。

4. 表面消费

OpenClaw 的其他部分会读取注册表,以暴露工具、通道、提供商配置、钩子、HTTP 路由、CLI 命令和服务。

具体到插件 CLI,根命令发现分为两个阶段:

  • 解析时元数据来自 registerCli(..., { descriptors: [...] })
  • 真正的插件 CLI 模块可以保持惰性,并在首次调用时注册

这样可以让插件拥有的 CLI 代码保留在插件内部,同时仍允许 OpenClaw 在解析前预留根命令名称。

重要的设计边界:

  • 清单/配置校验应基于 manifest/schema 元数据 工作,而不执行插件代码
  • 原生能力发现可以加载受信任的插件入口代码,以构建非激活的注册表快照
  • 原生运行时行为来自插件模块的 register(api) 路径,且 api.registrationMode === "full"

这种拆分让 OpenClaw 能够在完整运行时激活之前校验配置、解释缺失/禁用插件,并构建 UI/schema 提示。

插件元数据快照与查找表

一个 PluginCache 会在首次访问插件元数据时启动,包括 Gateway 启动前的 CLI 预检,并随着元数据和工件的需要逐步填充。Gateway 启动会保留该所有者,并构建其不可变的 PluginMetadataSnapshot。该快照包含所有已配置 agent 工作区的插件元数据,包括已禁用插件,并保留来源优先级和工作区溯源信息。它存储已安装插件索引、manifest 注册表、manifest 诊断信息、owner 映射以及插件 id 规范化器。包内容和惰性加载的模块导出属于同一缓存的其他类型化视图,而不是快照本身。

插件感知的配置校验、启动自动启用和 Gateway 插件引导会消费该快照,而不是独立重建 manifest/索引元数据。PluginLookUpTable 派生自同一快照,并为当前运行时配置添加启动插件计划。

通道设置目录会保留所请求的工作区和加载路径范围,包括原始插件影子,以便信任过滤可以选择合适的已安装替代项。

启动后,运行时读取器会复用该清单,而无需文件系统发现、manifest 重读或新鲜度检查。窄插件选择是同一清单的内存视图。更改账户或 agent 的运行工作区不会使其失效。显式插件生命周期操作会为安装、更新、移除、source 或 manifest 编辑以及发现根目录更改准备新清单,然后再将其发布到正在运行的 Gateway。

插件重载会在异步元数据准备之后协调配置监视器事件。未更改的 source 事件不会取消该操作;更新的写入或已更改的配置、安装记录或 source 所有权仍会取代它。

旧版 session-key 迁移会在检查通道存在性之前选择声明该能力的插件。根据迁移策略已符合条件的 owner 无需进行通道存在性探测。范围化选择仅针对其通道 owner 探测持久化凭据,因此在 Doctor 修复期间,无关的认证模块会保持未加载状态。该凭据范围不会限制基于环境的存在性信号:缺少插件的已配置通道仍会生成安装和恢复提示。

Model-id 规范化策略会随每个快照或窄化视图一起准备。模型选择、目录和运行时规范化会沿用该视图,而不是从其插件列表重建策略。空视图仍保持权威,并且不能从更广泛的进程快照继承策略。

Fleet 模型运行时准备会为每次构建捕获一个不可变配置和 authored-source 视图。插件参数处理、激活指纹和 agent 查找会跨 agent 复用这些捕获的事实。动态模型钩子仍会接收每个 agent 的目录、工作区和模型注册表。准备过程会在 agent 之间让出事件循环,以便大型 fleet 构建期间可以运行 Gateway 请求;配置刷新会创建新的捕获。现有安装无需配置更改或迁移。

快照和查找表将重复的启动决策保留在快速路径上:

  • 通道所有权
  • 启动插件规划
  • 启动插件 ID
  • 提供方和 CLI 后端所有权
  • 设置提供方、命令别名、模型目录提供方和清单契约所有权
  • 插件配置模式和通道配置模式验证
  • 启动自动启用决策

启动和热替换在所有已配置的 agent 工作区之间共享同一个已准备的注册表发布者和同一份清单。重新加载会保留工作区来源,因此当另一个插件发生变化时,未更改的链接插件不会被替换。替换会保留未更改的插件实例,并在排空受影响的服务和通道之前验证候选元数据。在等效元数据或设置中重新排序对象键不会替换注册;更改的值、有序列表和显式重新加载请求仍会替换。它会停止并释放之前的注册,然后再注册其替换项,随后一起发布运行时方法和元数据。已连接的客户端会在发布后刷新其插件能力。如果替换在发布前失败且清理成功,恢复会注册捕获的先前代码和配置,并带有新的资源所有权。发布后的失败会报告已提交的代次。插件运行时导入保持惰性;保留元数据不会激活每个已发现的插件。

持久的最终通道回复只有在它们确切的通道注册被保留时,才可以在无关重新加载之后使用准入 Gateway 的当前注册表。交接还要求通道设置、共享通道默认值和拥有插件的设置未更改,并且需要通道拥有的验证来保留已准入的发送方。Telegram 会检查其已解析的机器人凭据,并将其固定用于最终发送;更改的 token 文件内容、环境 Token 和 SecretRef 值不能选择另一个机器人。没有发送方准备的通道、新增或替换的通道注册、更改的设置以及正在关闭的 Gateway 仍保持阻止状态。这绝不会回退到另一个 Gateway,也绝不会重试可能已经到达提供方的发送。

替换会保留受影响的实例,即使 agent 轮次或未完成的清理仍保留它。已准备模型的替换门控会挂起新运行,同时已准入的运行使用其原始回调完成。新的顶层保留工作无法获取旧实例;已准入的消费者仍可以派生完成其运行所需的工作。详细的就绪状态和日志会报告保留工作数量和排空截止时间;最终 RPC 回执会报告应用以及任何排空通知。从实例自身活动回调发起的重新加载在准备期间仍会失败,以避免等待自身。空闲的已准备发布不会阻塞替换。

保留工作和进行中的调用共享 60 秒的停止前预算。保留运行会在其回调关闭之前完成;边车会在剩余有限消费者排空前释放其能力消费者。替换随后会在停止服务和通道之前暂停普通调用。空闲的服务和通道托管稍后与其所有者一起停止。如果工作未完成,重新加载会失败一次,恢复准入,并清除重新加载状态;先前插件代次会继续提供服务。截止时间不会取消 agent 运行,也不会允许释放未完成的写入。在该工作完成后重试 openclaw plugins reload <id>,或显式选择 --wait 以等待已准入工作稳定。显式等待属于其请求连接:Ctrl+C 或断开连接会取消发布前等待并执行相同的回滚。取消绝不会释放已准入工作、绕过清理或撤销已提交的发布。服务关闭和恢复保留其有界截止时间。成功发布会记录已应用的替换并发出 plugins.changed。

提供方或测试框架插件加载失败仍会记录在其运行时代次中。它会使该插件不可用,而不会取代该代次或阻塞使用健康插件的模型。使用 openclaw plugins inspect <id> --runtime --json 检查失败的拥有者。使用 openclaw doctor --fix 进行受支持的安装修复,或在插件代码中修复报告的问题,然后通过管理 Gateway API 请求 plugins.reload 以加载修复后的插件。

只读模型验证、有效工具清单和隔离模型探测在需要可执行的提供方或测试框架钩子时,会获取它们自己的注册。并发调用者共享已准备的代次,其生命周期释放器会在最终借用者以及任何未完成的准备或目录工作稳定后运行。取消不会在其回调仍在运行时关闭注册。进程关闭会在加入其剩余工作和释放之前撤销这些注册表视图。仅需元数据的目录读取不会获取这些可执行注册。有效工具清单只准备已配置和会话选择的模型事实,包括从已启用提供方捕获的目录;它不会刷新完整模型目录。会话选择不会更改已配置的模型选择器。

每个插件服务启动尝试都拥有一个清理操作,包括失败的启动。热替换以五秒截止时间观察候选启动和服务清理。候选启动失败会拒绝替换。替换已加载插件要求成功清理,之后另一个注册才能获取其资源;因此,待处理的清理或清理错误可能会拒绝替换并阻止自动恢复。待处理的启动会保留其资源,直到其完成且其唯一的停止操作稳定。插件移除可以报告延迟清理;Gateway 关闭会在释放插件资源之前加入该工作。释放在物理清理完成期间会停止新的注册调用。服务清理不会仅仅因为观察者超时而被第二次调用。如果命令目录刷新也停止了未更改的通道,失败的替换会恢复这些健康注册,同时失败的插件保持隔离。

A failed replacement automatically tries to restore the previous code and configuration. If its channels have already stopped, recovery observes pending service cleanup and admitted work once, for up to 60 seconds. Its observation has an independent Gateway-owned cancellation signal, so a disconnected reload requester cannot interrupt recovery. Only service-stop observer timeouts qualify for this wait; channel-stop failures, rejected service cleanup, and failed candidate cleanup still prevent recovery. The wait joins the original service stop promises, including services whose shared stop deadline was already exhausted. After cleanup and work settle successfully, recovery disposes the old registration, registers its captured code with fresh resource ownership, and restarts its channels. Manually stopped accounts stay stopped. A timeout never grants another registration ownership of an unfinished write's resources, and retries do not repeat service stop or resource disposal. Uncommitted failures also attempt to republish the prepared model runtime against the previous config, even when plugin recovery fails or cannot safely run, unless the Gateway is shutting down.

当替换失败时,系统会自动尝试恢复先前的代码和配置。如果其通道已经停止,恢复会观察待处理的服务清理和已接纳的工作一次,最长 60 秒。该观察具有独立的、由 Gateway 拥有的取消信号,因此已断开的重载请求方无法中断恢复。只有服务停止观察器超时才符合此等待条件;通道停止失败、被拒绝的服务清理和失败的候选清理仍会阻止恢复。该等待会加入原始的服务停止 Promise,包括共享停止截止时间已耗尽的服务。在清理和工作成功稳定后,恢复会释放旧注册,以新的资源所有权注册其捕获的代码,并重启其通道。手动停止的账户保持停止状态。超时永远不会将未完成写入的资源所有权授予另一个注册,并且重试不会重复执行服务停止或资源释放。未提交的失败也会尝试针对先前配置重新发布已准备好的模型运行时,即使插件恢复失败或无法安全运行,除非 Gateway 正在关闭。

When recovery cannot safely finish, the operation settles as failed and releases its channel reload pauses. Channel health reads retain captured account facts without invoking unavailable plugin code; healthy registrations can restart normally. Detailed readiness reports failing: ["plugin-reload"] with the affected plugin IDs and an actionable recovery reason, instead of leaving an indefinite "reloading" state. Retry openclaw plugins reload <id> after admitted work and pending cleanup settle, or restart the Gateway. Permanent cleanup failures still prevent another registration from acquiring those resources.

当恢复无法安全完成时,操作会以失败状态结束,并释放其通道重载暂停。通道健康读取会保留已捕获的账户事实,而不调用不可用的插件代码;健康的注册可以正常重启。详细的就绪报告会报告 failing: ["plugin-reload"],并包含受影响的插件 ID 和可操作的恢复原因,而不是停留在无限期的 “reloading” 状态。在已接纳的工作和待处理清理稳定后,重试 openclaw plugins reload <id>,或重启 Gateway。永久性清理失败仍会阻止另一个注册获取这些资源。

A later reload can retry after pending cleanup completes successfully, without capturing code from the disposed instance again. If draining fails after services or channels stop but before disposal starts, the quiesced registration retains its original loader for the next reload's recovery capture; ordinary plugin calls remain closed when recovery fails. Rejected replacements and failed recovery registrations retain the same cleanup barrier. Recovery preserves existing error records as diagnostics without executing their code. It never substitutes current package files for an already retired registration whose captured source has been released.

稍后的重载可以在待处理清理成功完成后重试,而无需再次从已释放的实例捕获代码。如果排空在服务或通道停止后、但在释放开始之前失败,已静默的注册会保留其原始加载器,用于下次重载的恢复捕获;当恢复失败时,普通插件调用保持关闭。被拒绝的替换和失败的恢复注册保留相同的清理屏障。恢复会将现有错误记录作为诊断信息保留,而不执行其代码。它绝不会用当前包文件替换一个已退役且其捕获源已释放的注册。

Missing captured source files produce a replacement warning instead of preventing a fresh plugin load. Recovery snapshots must contain every previously captured input, including companion files. If replacement then fails, recovery restores only plugins with available captured code; it does not substitute changed installed files for a missing snapshot. Existing call-drain and resource-cleanup requirements still apply.

缺失的捕获源文件会产生替换警告,而不是阻止新的插件加载。恢复快照必须包含所有先前捕获的输入,包括伴随文件。如果替换随后失败,恢复只会还原具有可用捕获代码的插件;它不会用已更改的已安装文件替换缺失的快照。现有的调用排空和资源清理要求仍然适用。

Gateway shutdown also joins actual harness, MCP, LSP, embedding, and media cleanup after their initial grace periods. When clearing the active registry, plugin host cleanup can advance to later hooks after a timeout, but registry resets and shared database closure wait for its actual completion. These waits preserve resources for cleanup; they do not restore a retired plugin's runtime authority.

Gateway 关闭还会在其初始宽限期之后加入实际的 harness、MCP、LSP、嵌入和媒体清理。在清除活动注册表时,插件宿主清理可以在超时后推进到后续钩子,但注册表重置和共享数据库关闭会等待其实际完成。这些等待为清理保留资源;它们不会恢复已退役插件的运行时权限。

Shutdown closes admission before waiting for config reloads to settle. Those reloads can still defer cleanup held by existing consumers. Final Gateway close releases those consumers, joins retained cleanup, and reports its failures before another Gateway can start.

关闭会在等待配置重载稳定之前关闭准入。这些重载仍可能延迟由现有消费者持有的清理。最终 Gateway 关闭会释放这些消费者,加入保留的清理,并在另一个 Gateway 可以启动之前报告其失败。

Gateway config reload and startup metadata persistence use process-bound plugin lifecycle leases. After a forced stop, the next process can reclaim a lease whose recorded same-host process identity is proven dead. Package installation leases retain their expiry protection because installer subprocesses can outlive their parent. A contended lease logs its holder and observed expiry while waiting; older leases without process identity must still expire before startup can safely refresh the plugin index. This uses the existing state schema and requires no update migration.

Gateway 配置重载和启动元数据持久化使用与进程绑定的插件生命周期租约。强制停止后,下一个进程可以回收一个租约,如果其记录的同主机进程身份被证明已死亡。包安装租约保留其过期保护,因为安装程序子进程可能比父进程存活更久。发生争用的租约在等待时会记录其持有者和观察到的过期时间;没有进程身份的旧租约仍必须过期,启动才能安全地刷新插件索引。这使用现有的状态模式,并且不需要更新迁移。

Executable CLI cleanup reports each disposer that exceeds five seconds and proceeds with later cleanup without canceling the pending work. On macOS with Node's system CA support enabled, automatic exit after command completion waits for this pending cleanup to finish. Explicit command exit requests and the update exit watchdog retain their bounded behavior.

可执行 CLI 清理会报告每个超过五秒的释放器,并继续执行后续清理,而不会取消待处理的工作。在启用了 Node 系统 CA 支持的 macOS 上,命令完成后的自动退出会等待此待处理清理完成。显式命令退出请求和更新退出看门狗保留其有界行为。

Standalone plugin and Codex supervision MCP stdio services retain their discovered registrations through accepted tool work, harness cleanup, and nested SDK provider lookups. Terminal shutdown cancels and joins handlers before releasing these registrations and awaiting their resource disposers. Transport-close and registration-disposal failures reach the serving caller. Programmatic servers created from supplied tools leave those resources with the caller; closing and reconnecting the same server does not dispose them.

独立插件和 Codex 监督 MCP stdio 服务通过已接纳的工具工作、harness 清理和嵌套 SDK 提供程序查找保留其发现的注册。终端关闭会在释放这些注册并等待其资源释放器之前取消并加入处理程序。传输关闭和注册释放失败会到达服务调用方。从提供的工具创建的程序化服务器会将这些资源留给调用方;关闭并重新连接同一服务器不会释放它们。

Hot registry publication does not wait for retired host cleanup; terminal shutdown also joins cleanup already started by earlier registry replacements before resetting shared state. Activation and rollback apply only to their captured registry version. An activation superseded by a lifecycle callback reports an error.

热注册表发布不会等待已退役宿主清理;终端关闭还会在重置共享状态之前加入先前注册表替换已启动的清理。激活和回滚仅适用于其捕获的注册表版本。被生命周期回调取代的激活会报告错误。

The cache rule is documented in Plugin architecture internals: Gateway retains one cache generation, while explicit management operations use isolated generations of the same cache. There are no wall-clock TTLs for Gateway metadata.

缓存规则记录在插件架构内部机制中:Gateway 保留一个缓存代,而显式管理操作使用同一缓存的隔离代。Gateway 元数据没有墙钟 TTL。

Install, update, registry refresh, and doctor flows may read fresh package metadata to validate their changes. A management snapshot or installed-index write alone does not replace the running Gateway's inventory: the Gateway lifecycle owner must prepare and publish it. Runtime flows use their selected snapshot or lookup table instead of falling back to cold management paths.

安装、更新、注册表刷新和 doctor 流程可以读取新的包元数据以验证其更改。仅管理快照或已安装索引写入不会替换正在运行的 Gateway 的清单:Gateway 生命周期所有者必须准备并发布它。运行时流程使用其选定的快照或查找表,而不是回退到冷管理路径。

运行时实例与源生命周期

推理验证会对运行时根据 source/build 偏好所选的工件进行指纹计算,包括显式的捆绑 source 覆盖。加载未更改的插件会保留该证明;更改其选定的运行时文件会使该证明失效。

受管运行时实例拥有其模块结果、已注册的可调用对象和 runtime-store 槽位。借助 Node 的同步模块钩子,它还拥有一个捕获的 source 工件。包插件在实例创建时捕获其包输入。独立文件捕获其入口和静态已知输入,而不会复制周围的工作区。已编译的捆绑运行时和 setup 模块共享宿主代码标识;每个清单仍拥有其已注册的回调和清理。替换该已编译代码需要构建并重启 Gateway。条件包别名保留其包元数据,原生 Node 条件从该捕获的元数据中选择目标。没有 exports 映射的旧版包也会接纳其现有的 main 或 index 入口,而不会执行未选中的代码。原生入口复用下文记录的接纳。所选包的其余主体在执行前被捕获。依赖链接保留现有的嵌套安装位置。安装在包旁边的依赖在捕获中仍保持同级关系,包括通过相对文件系统路径读取原生资产的可选平台包。其他祖先依赖链接到捕获的包根。捕获不会在单个 source 文件旁边添加 node_modules,因此 native-addon 加载器仍可以定位其包根及其构建资产。原生工件与其所属包目录一起被接纳,保留二进制的包相对路径和声明的伴随库依赖。没有已接纳包根的工件保留其包含目录。安装器拥有的目录使用硬链接或现有的保留目录引用;由插件安全检查检查的文件保留独立副本。可变 source 树为每个已接纳标识保留一个私有目录快照,通过就地编辑保留旧的二进制和伴随字节。该命名空间中的文件在接纳时准备;模块执行仍按需进行。注册共享接纳事实,但不共享其运行时权限。私有 Doctor 检查将其原生接纳事实与操作者的状态分开。它们的临时捕获在检查结束后绝不会成为对已安装索引的延迟写入;普通延迟写入保留其原始状态目录。当原生包共享依赖时,接纳仅为其自身的硬链接协调标识,即使文件系统的 ctime 未推进。记录的摘要在提升前会与已安装字节进行检查,包括先前作为独立副本捕获的伴随文件。未更改的伴随文件在 Doctor 和重新加载期间保持有效;source 内容检查仍会拒绝编辑。当文件符号链接不可用时,只有当其目录保留每个捕获的伴随文件和所选宿主 SDK 时,某一代可以使用硬链接。否则,该插件会报告一个加载错误,要求支持文件符号链接;更新将继续使用现有的插件失败警告行为。现有的 installed-index SQLite 负载记录目录成员关系、设备、inode、mode、大小、mtime 和 ctime 标识、SHA-256 摘要以及初始代回执。未更改的热启动复用这些事实。新增、删除或更改的伴随文件需要再次接纳。如果记录的捕获目录缺失,新的接纳会读取已安装包,而不会提升缺失捕获的标识。这也适用于使用旧更新器的更新后 Doctor。仅 ctime 的不确定性通过有界重新哈希解决,包括其 inode 被另一个捕获保留或释放的普通伴随文件。旧版重新加载回执保留其带框架的原始字节值,因此更改的回执仍需要流式传输其原生负载。只读检查在后续结算期间从不发布其原生接纳。延迟发布保留原始数据库权限和状态目录。Doctor 的私有检查将捕获的代码保存在 profile 限定的临时存储中,因此删除其数据库快照无法移除进程中仍加载的映像。完全缺失的已退役捕获可以从其已安装 source 重建;保留或部分存在命名空间中缺失的文件仍为加载错误。标识复用无法检测保留每个已记录标识字段的编辑。已接纳原生命名空间之外的 source 代码会被单独捕获和验证。已发布的原生捕获在普通临时清理中保留。Doctor 维护会删除未引用的捕获,同时保留 installed-index 引用、热代和存活所有者。系统临时回退捕获限定在其状态目录内;所有权未知的捕获会被保留。

每个捕获的代链接所选宿主 openclaw 包,以便从其模块启动的 Workers 和子进程可以解析宿主 SDK。该链接不依赖主线程的模块钩子,并在恢复期间重新创建。对已解析 SDK 文件 URL 和绝对路径的导入保持相同的宿主标识;它们不会创建宿主包或其运行时块的选择性副本。快照清理和更新 source 检查不会通过这些链接下降到宿主包中。

在现有运行时、setup 或可执行文件发现检查接纳一个入口后,其实例按需捕获导入的共享文件和依赖模块。相对、绝对和 file-URL 导入使用捕获的文件;TypeScript 依赖条目在其自身包作用域中编译。元数据和安装检查保持受限,并且不会获取可执行共享输入。

可执行文件加载可以通过单独捕获其选定模块来跟随共享模块链接。冷 source 快照仍会拒绝插件根之外的链接,因此延迟安装批次和安装摘要结算要求将这些输入打包为依赖。插件根之外的链接非模块资源不会被该模块加载路径捕获。

首次按需的 import() 或 require() 可能会观察到后续源码编辑;已捕获的元数据和入口字节保持不变。初始源摘要覆盖创建时的捕获;后续输入会扩展显式的源当前性检查,但不会改变该摘要。无效的可选包元数据仅在选中时才会失败。模块获取使用实例当前的准入状态,处置会关闭后续捕获。运行时和设置退役使用现有的五秒实例关闭预算。如果调用、保留的消费者或清理超过该预算,逻辑退役会返回强制退役诊断,并附带仍在运行的调用和消费者计数。普通调用权限关闭,迟到的成功结果会被拒绝。物理清理继续异步进行:已捕获的文件和模块解析器会保留,直到调用、消费者和清理尾部实际结束。显式保留的消费者会保持其已准入的轮次和清理权限,直到其宿主关闭或释放它。释放后,其回调和迟到结果会被拒绝。共享可写状态保持被拥有,直到其清理完成;迟到的清理失败仍然是资源交接的失败。Web 提供程序描述符保持其注册身份;运行时投影将其工厂和返回的工具绑定到所选插件实例。同步源检查和失败的捕获仍会在返回前清理。

默认源捕获位于 <stateDir>/tmp/plugin-captures/<instanceId>/captures/ 下,具有随机实例 ID,以及一个持有其原生生命周期租约的 owner.sqlite 令牌。Gateway 元数据及其源捕获保留同一进程本地实例;并发的 CLI 进程拥有单独的实例。释放一个捕获不能退役另一个捕获或仍在运行的元数据所有者。Gateway 元数据为其每小时清理提供调度器;可执行 CLI 命令通过其调用范围拥有维护。仅捕获源不会创建计时器。清理不会保留第一个命令的调用上下文。受管理的 tmp/plugin-captures 子树在状态目录位于插件源目录内时,会从源快照中排除。恢复仍可以从该子树内加载保留的源包。

可执行 CLI 命令在成功和失败时都通过现有调用资源范围退役其插件清单。由 Gateway 发布采用的清单会随 Gateway 元数据退役而保留。进程退出时,捕获所有者会同步退役任何仍持有本进程保管权的剩余实例,包括显式退出和 CLI 处理的信号。强制终止仍依赖启动回收。插件清理拥有此捕获子树;回收会在退役令牌前移除已捕获的有效载荷,因此部分删除仍可重试。令牌在其目录被移除之前关闭,包括在 Windows 上。

映射在当前进程中的原生库会保留其捕获和令牌,直到进程退出,即使其 JavaScript 模块缓存条目已被移除。清理会为该捕获生命周期记录一次 retained-by-loaded-module,而不是尝试解除已加载 Windows 映像的链接。未更改的原生包标识会继续在重新加载之间共享保留的有效载荷。原生加载尝试在初始化抛出异常时也会保留其捕获:初始化错误后原生映像可能仍保持映射。

在运行时插件加载之前,启动会在独占维护所有权下尝试感知回执的清理。它可以立即回收已退役且未加锁的实例,包括保留到前一个进程退出为止的未发布原生有效载荷。仍被已安装索引引用的已发布原生有效载荷保持可用。如果另一个进程持有状态所有权,这种机会性清理会静默跳过,而不要求操作员停止健康的 Gateway。其他维护或清理失败会产生警告,启动继续;观察性读取不会触碰捕获。

每小时清理也会检查此受管子树。实例在一小时后变得符合条件。仅凭年龄从不授权移除:对于携带令牌的实例,清理还必须获取现有令牌的独占原生租约,以证明保管权已释放。这在进程终止或重启后无需 PID 或启动命名空间记录即可工作,并为随附的 owner.sqlite 标记保留相同的清理契约。清理在移除前重新检查目录和令牌身份。活动租约、不可读条目、符号链接和无效令牌会保留文件。因分配中断或较早的部分清理而遗留且没有令牌的老化实例,会在成功重命名探测后被回收。分配在创建有效载荷前创建令牌,处置在移除其令牌前移除有效载荷。令牌属于其捕获实例;捕获不会创建全局协调数据库。移除仍保持异步且为建议性。此子树从状态备份中排除,因为其捕获的包字节可重建。

元数据保留不会在需要源捕获之前创建目录。如果状态目录无法接受捕获,加载会回退到隔离的系统临时实例并报告警告。正常处置仍会移除该实例;自动清理不会扫描无关的系统临时根。没有总磁盘配额,活动实例可能合理地超过一小时清理宽限期。如果其有效载荷目录在实例仍持有保管权时被移除,下一次捕获会在同一租约下重新创建该目录。这不会恢复先前删除的已捕获文件,也不会重新创建缺失的所有权目录。

启动和每小时清理还会回收所选状态的临时目录和当前系统临时目录中无令牌的 openclaw-plugin-build-* 和 openclaw-model-catalog-* 根。根必须存在超过一小时且没有保管令牌。在 macOS 和 Linux 上,清理会在重命名前立即重新检查每个旧根是否属于当前 UID,即使在特权运行中也保留其他用户的捕获。Windows 没有等效的 UID 检查,因此特权 Windows 清理保留以下年龄和重命名探测规则。能够找到另一个 OpenClaw 生产者的完整进程普查会保留旧根。当普查不可用时(包括在 Windows 上),清理改用年龄和重命名探测;共享冲突会将锁定根留给稍后的周期。这是对可重建旧临时数据的尽力清理,而不是证明较旧生产者已停止使用它的证据。

系统临时目录中较旧的 openclaw-plugin-build-* 目录没有所有者记录,无法证明其生产者是否仍然存活。Doctor 会报告以下位置下的无 token openclaw-plugin-build-* 和 openclaw-model-catalog-* 根目录:状态临时目录 ~/.openclaw/tmp(即使选择了其他状态目录)、POSIX 主机上的当前系统临时目录 /tmp,以及已记录的托管服务 TMPDIR 位置。它会去重目录别名,并报告每个捕获的路径和常规文件大小,而不会跟踪捕获内部的链接。目录根的报告大小和移除回执中包含所有嵌套的 openclaw-plugin-build-* 树。

openclaw doctor --fix 仅当 Doctor 持有 Gateway 维护权限,并且完整的主机进程普查未发现其他 OpenClaw 生产者时,才会回收这些遗留根目录。该规则在每次移除前重新检查这两个条件,并打印一份回执,列出已移除的路径及其大小。如果存在存活同级进程、普查不可用或缺少维护权限,捕获将保留在原位,并附带说明性消息。当前进程期间创建或修改的捕获以及携带 token 的捕获保持不变。Linux 和 macOS 在检查进程时保留原生参数边界。macOS 还可以通过内核可执行文件路径和有效的 Apple 平台签名识别以其他用户身份运行的原生系统服务;其他存活进程不可用的参数会使清理保持阻塞并给出原因。在无法获得完整进程普查的主机上(包括 Windows 和已识别的容器环境),Doctor 会报告遗留捕获,但跳过其移除。对于共享主机临时目录的容器,请在停止其 OpenClaw 容器后在主机上运行维护。Doctor 的维护修复与运行时回收保持分离:其更广泛的清单包括当前运行时未使用的旧服务临时位置。现代捕获保留其托管 token 清理。

已配置的 Gateway 代理在每个插件清单生命周期内共享一个模型目录工作进程。代理和认证事实属于每个任务;插件注册和捕获源保留在共享清单中。提供自身环境的独立主机为该环境保留一个隔离的目录工作进程。当所选运行时实例的捕获源已加载时,提供程序发现条目使用该精确运行时实例的捕获源,因此发现不会为同一插件包创建第二份副本。独立发现保持其自身的设置生命周期。每个工作进程为每个加载器工作区保留当前插件注册上下文,由配置、环境和插件清单匹配的代理共享。交替的未变更工作区复用其捕获源;替换一个工作区不会驱逐另一个。Node 在捕获文件被移除后仍保留原生 ESM 模块图,直到工作进程退役,因此实际的源或配置修订仍可能在该生命周期内保留模块内存。代理凭据和已配置模型事实随每个请求传递;目录任务不会重建代理工作区。发现复用该上下文已获取的注册。第一个目录请求会一起准备代理已知已配置和凭据提供程序的注册;只有请求的提供程序运行目录钩子。新观察到的所有者扩展该上下文,而不丢弃较早的所有者。替换在已接纳的工作稳定后释放它们。成功处置的注册保留其插件缓存。

目录观察是被动的。清单请求可以要求目录所有者更新过期的提供程序,同时返回其已接受的行。聊天元数据和会话投影只观察发布,因此刷新不能通过其自身通知来调度自身。所选原生模型发现拥有独立的获取所有者,并且不等待提供程序清单更新。两个所有者在发布前将其结果与最新已接受的对应方合并。目录工作进程使用 512 MiB V8 旧生代限制,而不是继承 Gateway 的默认堆预算。显式的进程级堆标志会覆盖此限制;原生和外部分配不在其范围内。

目录和认证刷新任务携带主机准备好的 Claw 同意来源信息。工作进程配置重建和提供程序导入使用这些事实,而无需打开或复制共享状态数据库。主机配置发布和 Doctor 保留其现有的来源刷新和保留工件的检查路径;存储的数据、架构以及更新/回滚行为保持不变。

凭据持久化在凭据发现之前发布新的共享存储所有权。登录和显式认证刷新加入凭据所有者的发布,而不是为同一变更创建另一个目录代际。

模型目录工作进程将其捕获的插件文件保留在同一托管捕获实例下的工作进程拥有的目录中,托管权由其生产者保留。父进程在该工作进程退出后移除任何剩余捕获,包括取消和崩溃。文件在工作进程运行期间保持可用,退役一个工作进程不会移除另一个代际的捕获。如果整个 Gateway 被终止,现有每小时清理只有在能够获取现有 token 的独占原生租约时,才会回收被遗弃的实例。同一规则涵盖随附的 SQLite 标记,并在重启后有效。仅凭年龄永远不会释放捕获。取消在工作进程退出后释放计算容量;终止关闭也会等待文件清理。文件移除失败会作为清理警告报告。

仅加载元数据不会执行每个插件,并且注册保持同步。同步加载的 TypeScript 条目及其同步 TypeScript 导入保留 Jiti 的 CommonJS 编译行为,包括 .mts 和 .mtsx 条目。Node 评估捕获的输出。请将顶层 await 排除在同步入口点之外;通过生命周期回调或稍后的动态导入启动异步工作。

动态 TypeScript 导入保留异步 CommonJS 执行,包括 顶层 await 和 module.exports,而从原生 JavaScript 加载的源 遵循 Node 的模块格式。首次求值会为该实例固定该模式; 仅解析模块并不会求值它。源 import.meta.resolve 保留 Jiti 的可选父 URL 和解析选项, 包括自定义条件和 try。单参数解析器使用 源的目录和包作用域。

从捕获源加载的条目会为其实例保留求值失败, 而不是通过另一个加载器重试。核心随附的 JavaScript 以及在捕获插件实例之外加载的库保持其现有的原生/Jiti 加载行为。

宿主 Plugin SDK 始终保持在宿主的原生模块图上,包括 当 Jiti 编译插件入口时。SDK 别名使用规范文件系统路径,因此 符号链接检出无法创建另一个宿主所有者。运行中的宿主选择 源 SDK 模块或已构建 SDK 模块;插件的文件扩展名不会选择第二个 SDK 图。源宿主需要一个原生 TypeScript 加载器,例如仓库的 工具预加载。无法原生加载的 SDK 会失败,而不是再次被 插件转换器求值。插件重新加载可能会创建插件 私有代码的新实例,而现有插件和替换插件共享宿主 SDK 身份和权限。

捕获的包还保留到所选宿主安装位置的链接,以便子 工作器和进程可以导入其公共 SDK。这些独立隔离区使用 安装位置的常规包导出;它们不会继承父级的源 别名或权限。捕获处置会移除该链接,而不会移除宿主包。

受管理的 TypeScript 文件名元数据(import.meta.url、import.meta.filename、 import.meta.dirname、__filename 和 __dirname)标识捕获的 源,以便相对资源读取保持在该代内。Node 从单独目录执行编译后的 JavaScript;其模块 URL 和 CommonJS 缓存键可能 与源文件名不同。

Bun 1.4.2 使用其原生/Jiti 加载器,并为每个受管理实例使用 单独的捕获源工件。重新加载会准备新的 TypeScript 条目和辅助函数,而 现有消费者保留其旧实例。处置会移除该实例的 捕获缓存记录和文件,而不会驱逐其替换项或宿主 SDK。 原生导入和 Jiti 导入保留各自的包条件。

Bun 需要本地包的导入/导出目标在原生解析之前存在。 因此,选择性捕获会在求值之前获取由这些声明匹配的现有文件, 包括条件分支和通配符目标。未选中的 源保持为原始字节;其代码和 TypeScript 配置不会被求值。 这可能在启动时读取比 Node 的按需捕获更多的文件。其他 延迟导入仍会通过实例的当前准入在首次使用时获取源; 已准备好的模块无需新的获取。

当使用 Jiti 的 TypeScript 路径设置时,在插件活动期间保持原始 tsconfig 文件和 配置依赖可用。已加载模块 保留其选定的路径映射;先前未访问的模块可能在首次使用时读取这些 配置文件。新加载的实例选择当前 路径设置。

注册表退役将受管理的执行权限与物理资源释放分开撤销。 已获取的检查可以释放其执行权限,同时 借用者仍持有底层注册资源;最后一个物理 声明负责其处置,包括在释放请求结束后仍可用的 清理工作范围。裸 SDK 提供程序结果保留其自身的实例 消费者,因此其回调在拥有它们的 SDK 宿主关闭之前保持可用。 该宿主在释放消费者和资源之前加入已准入的回调工作; 释放检查仍会阻止新的借用。网关关闭会保留共享依赖, 直到仍需要它们的宿主加入。这些所有权规则不会 使原生插件成为沙箱,也不会自动关闭插件创建的资源。 请参阅插件生命周期和清理 了解插件作者的清理契约。

已准入的代理轮次在已接受的提交和 引擎处置期间保持其原始上下文引擎。重新加载可以在该轮次完成时报告延迟清理。 开始引擎处置会立即关闭其常规回调;引擎的 清理保持受所有,直到其完成。

激活规划

激活规划是控制平面的一部分。调用方可以在加载更广泛的运行时注册表之前,询问哪些插件与具体命令、提供程序、通道、路由、代理框架或能力相关。

规划器保持当前清单行为兼容:

  • activation.* 字段是显式规划器提示
  • providers、channels、commandAliases、setup.providers、contracts.tools 和钩子保持为清单所有权回退
  • 仅 ids 的规划器 API 对现有调用方保持可用
  • 计划 API 报告原因标签,以便诊断能够区分显式提示和所有权回退

Warning

不要将 activation 视为生命周期钩子或 register(...) 的替代。它是用于缩小加载范围的元数据。当所有权字段已经描述关系时,优先使用所有权字段;仅将 activation 用作额外的规划器提示。

通道插件与共享消息工具

通道插件无需为常规聊天操作注册单独的发送/编辑/反应工具。OpenClaw 在核心中保留一个共享的 message 工具,通道插件负责其背后的通道特定发现和执行。

当前边界是:

  • 核心拥有共享 message 工具宿主、提示接线、会话/线程簿记和执行分发
  • 通道插件拥有范围化操作发现、能力发现以及任何通道特定模式片段
  • 通道插件拥有提供程序特定的会话对话语法,例如对话 id 如何编码线程 id 或从父对话继承
  • 通道插件通过其操作适配器执行最终操作

For channel plugins, the SDK surface is ChannelMessageActionAdapter.describeMessageTool(...). That unified discovery call lets a plugin return its visible actions, capabilities, and schema contributions together so those pieces do not drift apart.

Message action names use a deliberately closed, core-owned vocabulary so every transport can render every action. Plugins add action names through a core PR; runtime registration is intentionally unsupported.

When a channel-specific message-tool param carries a media source such as a local path or remote media URL, the plugin should also return mediaSourceParams from describeMessageTool(...). Core uses that explicit list to apply sandbox path normalization and outbound media-access hints without hardcoding plugin-owned param names. Prefer action-scoped maps there, not one channel-wide flat list, so a profile-only media param does not get normalized on unrelated actions like send.

Core passes runtime scope into that discovery step. Important fields include:

  • accountId
  • currentChannelId
  • chatType (direct, group, or channel when the inbound route establishes it)
  • currentThreadTs
  • currentMessageId
  • sessionKey
  • sessionId
  • agentId
  • trusted inbound requesterSenderId

That matters for context-sensitive plugins. A channel can hide or expose message actions based on the active account, current room/thread/message, authoritative conversation type, or trusted requester identity without hardcoding channel-specific branches in the core message tool. Treat chatType as discovery scope supplied by the current inbound route, not something to infer again from an opaque channel id; it is absent when that route did not establish the conversation type.

This is why embedded-runner routing changes are still plugin work: the runner is responsible for forwarding the current chat/session identity into the plugin discovery boundary so the shared message tool exposes the right channel-owned surface for the current turn.

For channel-owned execution helpers, channel plugins should keep the execution runtime inside their own plugin modules. Core no longer owns the Discord, Slack, Telegram, or WhatsApp message-action runtimes under src/agents/tools. We do not publish separate plugin-sdk/*-action-runtime subpaths, and those plugins should import their own local runtime code directly from their plugin-owned modules.

The same boundary applies to provider-named SDK seams in general: core should not import channel-specific convenience barrels for Discord, Signal, Slack, WhatsApp, or similar plugins. If core needs a behavior, either consume the bundled plugin's own api.ts / runtime-api.ts barrel or promote the need into a narrow generic capability in the shared SDK.

Bundled plugins follow the same rule. A bundled plugin's runtime-api.ts should not re-export its own branded openclaw/plugin-sdk/<plugin-id> facade. Those branded facades remain compatibility shims for external plugins and older consumers, but bundled plugins should use local exports plus narrow generic SDK subpaths such as openclaw/plugin-sdk/channel-policy, openclaw/plugin-sdk/runtime-store, or openclaw/plugin-sdk/webhook-ingress. New code should not add plugin-id-specific SDK facades unless the compatibility boundary for an existing external ecosystem requires it.

For polls specifically, there are two execution paths:

  • outbound.sendPoll is the shared baseline for channels that fit the common poll model
  • actions.handleAction("poll") is the preferred path for channel-specific poll semantics or extra poll parameters

Core now defers shared poll parsing until after plugin poll dispatch declines the action, so plugin-owned poll handlers can accept channel-specific poll fields without being blocked by the generic poll parser first.

See 插件架构内部 for the full startup sequence.

能力所有权模型

OpenClaw 将原生插件视为 公司 或 功能 的所有权边界,而不是无关集成的大杂烩。

这意味着:

  • 公司插件通常应拥有该公司所有面向 OpenClaw 的表面
  • 功能插件通常应拥有其引入的完整功能表面
  • 频道应消费共享核心能力,而不是临时重新实现 provider 行为
供应商多能力

google 拥有文本推理、CLI 后端、嵌入、语音、实时语音、媒体理解、图像/音乐/视频生成和网络搜索。openai 拥有文本推理、嵌入、语音、实时转录、实时语音、媒体理解和图像生成。minimax 拥有文本推理,以及媒体理解、语音、图像/音乐/视频生成和网络搜索。

供应商单能力

arcee 和 chutes 仅拥有文本推理;microsoft 仅拥有语音。供应商插件可以保持如此窄的范围,直到它需要覆盖该供应商更多的表面。

功能插件

voice-call 拥有呼叫传输、工具、CLI、路由和 Twilio 媒体流桥接,但消费共享语音、实时转录和实时语音能力,而不是直接导入供应商插件。

预期最终状态是:

  • 供应商面向 OpenClaw 的表面位于一个插件中,即使它跨越文本模型、语音、图像和视频
  • 其他供应商也可以为自己的表面区域这样做
  • 频道不关心哪个供应商插件拥有 provider;它们消费核心暴露的共享能力契约

这是关键区别:

  • 插件 = 所有权边界
  • 能力 = 多个插件可以实现或消费的核心契约

因此,如果 OpenClaw 添加一个新领域,例如视频,第一个问题不是“哪个 provider 应该硬编码视频处理?”第一个问题是“核心视频能力契约是什么?”一旦该契约存在,供应商插件就可以针对它注册,频道/功能插件就可以消费它。

如果该能力尚不存在,通常正确的做法是:

1. 定义能力

在核心中定义缺失的能力。

2. 通过 SDK 暴露

通过插件 API/运行时以类型化方式暴露它。

3. 接入消费者

将渠道/功能接入该能力。

4. 供应商实现

让供应商插件注册实现。

这样可保持所有权明确,同时避免核心行为依赖单一供应商或一次性的插件特定代码路径。

能力分层

在决定代码应放在哪里时,可使用以下心智模型:

共享编排、策略、回退、配置合并规则、投递语义以及类型化契约。

供应商特定 API、认证、模型目录、语音合成、图像生成、视频后端、用量端点。

Discord/Slack/voice-call 等集成,消费核心能力并将其呈现在某个能力面上。

例如,TTS 遵循这种形态:

  • 核心拥有回复时 TTS 策略、回退顺序、偏好设置和渠道投递
  • elevenlabs、google、microsoft 和 openai 拥有合成实现
  • voice-call 消费电话 TTS 运行时辅助函数

未来能力也应优先采用相同模式。

多能力公司插件示例

公司插件从外部看应具有内聚性。如果 OpenClaw 为模型、语音、实时转录、实时语音、媒体理解、图像生成、视频生成、网页抓取和网页搜索提供了共享契约,供应商就可以在一个地方拥有其所有能力面:

import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { exampleAiMedia } from "./exampleai-media.js";

export default definePluginEntry({
  id: "exampleai",
  name: "ExampleAI",
  description: "ExampleAI models and media capabilities.",
  register(api) {
    api.registerProvider({
      id: "exampleai",
      // auth/model catalog/runtime hooks
    });

    api.registerSpeechProvider({
      id: "exampleai",
      // vendor speech config — implement the SpeechProviderPlugin interface directly
    });

    api.registerMediaUnderstandingProvider({
      id: "exampleai",
      capabilities: ["image", "audio", "video"],
      describeImage: (req) => exampleAiMedia.describeImage(req),
      transcribeAudio: (req) => exampleAiMedia.transcribeAudio(req),
      describeVideo: (req) => exampleAiMedia.describeVideo(req),
    });

    api.registerWebSearchProvider({
      id: "exampleai-search",
      createTool() {
        // Return the vendor-owned web search tool.
      },
    });
  },
});

重要的不是确切的辅助函数名称,而是形态:

  • 一个插件拥有供应商能力面
  • 核心仍然拥有能力契约
  • 提供方请求转换和 HTTP 辅助函数保留在供应商插件中
  • 渠道和功能插件消费 api.runtime.* 辅助函数,而不是供应商代码
  • 契约测试可以断言插件已注册其声称拥有的能力

能力示例:视频理解

OpenClaw 已将图像/音频/视频理解视为一个共享能力。相同的所有权模型也适用于该场景:

1. 核心定义契约

核心定义媒体理解契约。

2. 供应商插件注册

供应商插件按适用情况注册 describeImage、transcribeAudio 和 describeVideo。

3. 消费者使用共享行为

渠道和功能插件消费共享的核心行为,而不是直接接入供应商代码。

这可避免将某个提供方的视频假设固化到核心中。插件拥有供应商能力面;核心拥有能力契约和回退行为。

视频生成已经使用相同流程:核心拥有类型化能力契约和运行时辅助函数,供应商插件针对它注册 api.registerVideoGenerationProvider(...) 实现。

需要具体的落地清单吗?参见 添加能力。

契约与强制

插件 API 表面被有意设计为类型化,并集中在 OpenClawPluginApi 中。该契约定义了支持的注册点以及插件可依赖的运行时辅助函数。

为什么这很重要:

  • 插件作者获得一个稳定的内部标准
  • 核心可以拒绝重复所有权,例如两个插件注册相同的提供方 id
  • 启动时可以为格式错误的注册呈现可操作的诊断信息
  • 契约测试可以强制捆绑插件的所有权,并防止静默漂移

存在两层强制机制:

运行时注册强制

插件注册表会在插件加载时验证注册。示例:重复的提供方 id、重复的语音提供方 id 以及格式错误的注册会产生插件诊断信息,而不是未定义行为。

契约测试

捆绑插件会在测试运行期间被捕获到契约注册表中,以便 OpenClaw 显式断言所有权。目前这用于模型提供方、语音提供方、网页搜索提供方以及捆绑注册所有权。

实际效果是,OpenClaw 可以提前知道哪个插件拥有哪个能力面。由于所有权是声明式、类型化且可测试的,而不是隐式的,因此核心和渠道可以无缝组合。

契约中应包含什么

  • 类型化
  • 小型
  • 能力特定
  • 由核心拥有
  • 可被多个插件复用
  • 渠道/功能无需供应商知识即可消费
  • 隐藏在核心中的供应商特定策略
  • 绕过注册表的一次性插件逃生舱
  • 渠道代码直接触达供应商实现
  • 不属于 OpenClawPluginApi 或 api.runtime 的临时运行时对象

如有疑问,请提高抽象层级:先定义能力,然后让插件接入它。

技能预览

Control UI 会预览已声明的插件技能,而无需安装或执行它。 打开预览会读取其文件清单以及入口 SKILL.md 正文。 选择另一个文件会按需读取该正文;导航永远不会预取 同级内容。在所选文件加载期间,文件树仍然可用, 失败的读取可以就地重试。

目录读取始终固定到所选软件包版本,并验证清单中的路径、大小和 SHA-256 哈希值。已安装读取始终保持在已解析的插件根目录内,拒绝不安全的链接,并拒绝已更改的插件版本。两种路径都保留文件数量、树深度、单文件和聚合捆绑包限制。聚合限制适用于清单,而不是选择顺序。

已加载的主体和待处理读取属于一个打开的预览和 Gateway 连接。关闭、重新打开、导航离开或重新连接会使该缓存失效。迟到的文件响应可以填充其自身的缓存条目,但不能更改所选文件。新的所选文件请求会重新验证清单;只有已加载的主体会被复用。就地编辑的已安装文件在重新打开时会可见。

执行模型

原生 OpenClaw 插件以进程内方式与 Gateway 运行。它们没有沙箱隔离。已加载的原生插件与核心代码具有相同的进程级信任边界。

Warning

原生插件的影响:插件可以注册工具、网络处理器、钩子和服务;插件缺陷可能导致 Gateway 崩溃或不稳定;恶意原生插件等同于在 OpenClaw 进程内执行任意代码。

兼容捆绑包默认更安全,因为 OpenClaw 当前将它们视为元数据/内容包。在当前版本中,这主要指捆绑技能。

对于非捆绑插件,请使用允许列表和显式安装/加载路径。将工作区插件视为开发时代码,而不是生产默认项。

对于捆绑工作区软件包名称,请保持插件 ID 锚定在 npm 名称中:默认使用 @openclaw/<id>,或者当软件包有意暴露更窄的插件角色时,使用已批准的类型化后缀,例如 -provider、-plugin、-speech、-sandbox 或 -media-understanding。

Note

信任说明: plugins.allow 允许加载插件 ID;它不会验证来源出处,也不会选择加载哪个相同 ID 的副本。自动发现的工作区插件不会仅仅因为该 ID 已启用或列入允许列表而遮蔽捆绑插件。

对于有意进行的本地覆盖,请使用 plugins.load.paths 选择插件路径。受跟踪的全局安装也可以覆盖普通捆绑副本。在源码安装中,与主机一起构建的插件优先于受跟踪的全局插件,即使 OPENCLAW_DEV_SOURCE_ROOT 未设置也是如此。仅软件包版本匹配并不能证明注册表插件与源码构建的 SDK 相匹配。有关完整顺序,请参阅 发现优先级。

指向主机自身捆绑插件树的已配置路径或安装记录保留捆绑来源,包括源码和编译条目;不同的本地副本不会因其名称或允许列表条目而继承信任。检出运行器会自动提供开发选择器,包括针对编译插件。请参阅 开发调试。

捆绑插件的信任从源码快照解析——加载时磁盘上的清单和代码——而不是从安装元数据解析。损坏或被替换的安装记录无法悄悄扩大捆绑插件的信任面,超出实际源码所声明的范围。

导出边界

OpenClaw 导出的是能力,而不是实现便利性。

保持能力注册公开。精简非契约辅助导出:

  • 捆绑插件特定的辅助子路径
  • 不打算作为公共 API 的运行时管道子路径
  • 供应商特定的便捷辅助函数
  • 属于实现细节的设置/入门辅助函数

保留的捆绑插件辅助子路径已从生成的 SDK 导出映射中退役。将所有者特定的辅助函数保留在所属插件软件包内;只将可复用的主机行为提升为通用 SDK 契约,例如 plugin-sdk/gateway-runtime、plugin-sdk/security-runtime 以及注入的插件 API 能力。

内部实现与参考

有关加载管道、注册表模型、提供者运行时钩子、Gateway HTTP 路由、消息工具模式、通道目标解析、提供者目录、上下文引擎插件以及添加新能力的指南,请参阅 插件架构内部实现。

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