跳转至

持久化入口

让入站投递具备持久性,并让重放、保留和重启行为可证明。本文是构建渠道插件指南的一部分。

入站入口(实验性)

迁移入站授权的渠道可以在运行时接收路径中使用实验性的 openclaw/plugin-sdk/channel-ingress-runtime 子路径。它接收平台事实、原始允许列表、路由描述符、命令事实和访问组配置,然后返回发送者/路由/命令/激活投影以及有序的入站图,而平台查找和副作用仍保留在插件中。请将插件身份归一化保留在你传给解析器的描述符中;不要从解析后的状态或决策中序列化原始匹配值。API 设计、所有权边界和测试预期请参阅渠道入站 API。

将确切的解析器结果作为 channelIngress 传给宿主注入的已注册上下文构建器。用于执行的结果必须包含最终的 agent/session/message/event contextBinding;仅用于决策的解析器调用可以省略它。这样可以保留原生插件中与记录、代次和作用域绑定的参与者证据,并通过一次性排队运行准入传递,而不会将其暴露在消息上下文字段中。独立的公共构建器不是权威替代品。切勿根据发送者、路由、房间、账户、线程、消息、传输或会话值重建证据。旧版适配器只有在路径经源码证明缺少权威入站解析器集成时,才能显式传递 channelIngress: "unsupported"。受支持的路径必须传递确切结果;省略属于无效的生产接线。缺失、伪造、过期、重用或混合的受支持证据会投影为 unknown,绝不会作为允许信号。

持久化入站与重放去重

采用持久化入站的渠道应使用 openclaw/plugin-sdk/channel-outbound 中的 createChannelIngressMonitor,除非它们需要契约在准入或泵送方式上有实质不同。在单个接收瓶颈处将原始传输信封入队(接收时不进行归一化);对于 webhook 传输,将传输 ack 门控在持久化追加之上;为每个会话派生一个串行化通道;并在派发采用时将事件标记为完成。队列的主键是 (queue_name, event_id),完成操作会对该行打墓碑而不是删除,因此平台延迟重投的同一 event_id 会在墓碑保留窗口内被持久化拒绝。监控 API 与关闭契约请参阅渠道出站 API。

该墓碑是重放防护(openclaw/plugin-sdk/persistent-dedupe)的分层规则:已排空渠道只有在防护的身份标识或保留期超过队列自身的身份标识或保留期时,才保留单独的重放防护——即一个不同于传输投递 id 的逻辑消息键(Telegram 对 chat_id:message_id 去重,因为防抖合并可能会在全新的 update_id 下重新呈现消息),或一个比渠道墓碑保留时间更长的窗口。如果防护键与排空操作的 event_id 相同,请在采用该排空时删除防护,并改为设置 completedTtlMs/completedMaxEntries 以覆盖旧防护窗口。非去重保护(例如年龄围栏)与本规则无关。稳定的出站消息 ID 使用 openclaw/plugin-sdk/channel-outbound 中的共享出站回显注册表,而不是渠道本地 TTL 缓存。

持久化重放防护会在共享状态工作器中等待 SQLite 读取、比较、写入和旧文件迁移。竞争记录会在写入事务中再次比较;清空内存或遗忘某个键会隔离较早的异步缓存填充。在确认采用或完成清理之前,请等待提交和删除完成。错误钩子保留其现有策略:抛错的钩子拒绝操作,不抛错的钩子允许使用防护的内存回退。工作器故障绝不会将持久化切换为同步 SQLite。多键提交和删除会在返回错误前结算所有已接受的写入,因此回滚和关闭不会与仍在运行的兄弟变更操作竞争。

传输类别与保留策略

按接收边界的恢复保证对传输进行分类:

  • Ack 门控的 webhook 或事件投递: 仅在持久化追加完成后确认或返回成功。追加失败必须让投递仍可重试,或者直接使接收边界失败。此类包括 Slack、SMS、Zalo、Microsoft Teams、Google Chat、LINE 和 Synology Chat。
  • 等待型轮询或流式投递: 仅在追加完成后推进远程游标或发送传输 ack。当不存在显式游标时,保持接收回调串行化并被等待,这样追加失败不会让接收循环超前运行。Telegram 轮询、Signal 和 Tlon 属于此类;Telegram webhook 投递遵循上面的 ack 门控规则。
  • 不可重放套接字: IRC、Mattermost、Twitch 和 Zalo Personal 无法要求平台重新投递已接受的事件。它们的持久化队列保护进程崩溃窗口并支持本地重启恢复;完成墓碑对平台重放几乎不起作用。

将 30 天作为全集群的墓碑 TTL 约定,而不是 SDK 默认值。高吞吐的重投窗口通常使用 20,000 条已完成记录的上限;低吞吐的等待型和非重放传输通常使用 1,000-2,000。当前例外包括 LINE 的 4,096 条上限、SMS 的 24 小时完成 TTL,以及 Tlon 的仅上限完成保留。失败行的上限也可能低于完成行的上限。TTL 和上限都会修剪行,因此有效保留期以先达到的边界为准。只有为了有据可查的平台重试时间范围、需要保留的已发布重放防护窗口、预期容量或磁盘预算,或非重放传输,才可以偏离;并用测试覆盖保留契约。

至少一次副作用

排空派发会在入站行到达其完成墓碑之前运行命令副作用。如果进程在这两个步骤之间崩溃,该行会被重放,副作用可能再次执行。这个至少一次崩溃窗口是默认契约。对于非幂等工作,例如配置写入、存储清理或回复通道之外的可见确认,请使用 openclaw/plugin-sdk/ingress-effect-once 中的 createIngressEffectOnce(...)。每次调用都要传入稳定的入站 eventId 加一个效果名称。为每个入站队列/账户创建一个辅助函数,并为该作用域使用稳定且唯一的 namespacePrefix,因为传输事件 ID 可能是队列局部的。辅助函数只在效果成功后提交其持久化声明;抛错的效果会释放该声明,以便排空重试可以再次执行;并发调用者会等待活动声明。持久化状态错误会在提供了 onDiskError 时调用它并拒绝操作,而不是回退到进程内存。

将辅助器的 ttlMs 设置为至少等于通道的入口墓碑保留期,加上效果提交与行完成之间的最大延迟,包括有界停机和排空重试。效果记录的 TTL 从提交时开始,而墓碑保留期则从完成时开始;如果待处理行的生命周期无界,则任何有限 TTL 都无法覆盖任意停机。当墓碑无法再重放该行后,较旧的效果记录就是无用负担。为保留窗口内可能存在的每个不同事件/效果键设置 stateMaxEntries,并考虑队列的已完成条目上限以及每个事件的最大效果数。更低的上限会在其 TTL 到期前驱逐最旧记录,并允许该效果再次执行。如果进程在效果成功之后、claim 提交之前死亡或持久化失败,或者记录在其入口行仍待处理时过期,则仍会残留至少一次窗口。

动态策略发布

Gateway 回复分发会为每个新回合选择当前已提交的 model-runtime 配置和目录,包括来自保留了启动配置的监视器的低级 channel.reply.dispatchReplyFromConfig 调用。分发会在准入前等待正在进行的 model-runtime 发布。通道传输和访问策略的新鲜度仍属于账户监视器;回复分发不会替代持久入口或其先追加后确认契约。 旧版 usePublishedModelRuntime 参数仍为 SDK 兼容性而接受,但不再控制 Gateway 模型准入。

仅对消费者读取已提交运行时配置而不替换通道资源的字段使用 reload.noopPrefixes。这些写入仍会发布经过验证的运行时快照;“noop” 表示不重启组件。* 路径段匹配一个非空配置键,例如 channels.example.accounts.*.allowFrom。更深的边界优先;在同一深度,精确路径优先于通配符。

在账户启动时绑定 createRuntimeConfigReader,并在每次新准入时派生一致的策略快照。将已解析名称缓存与该账户所有者一起保留,并在异步解析后重新检查当前修订版本。不要在另一条消息或交互路径中保留仅启动时使用的允许列表。对于异步 shouldSupersedePending 授权,返回一个同步守卫,以验证已准备的策略仍然有效。排空会在取消采用前工作之前立即调用此守卫;对于没有异步权限解析的谓词,仍支持布尔决策。

将凭据、传输设置和账户生命周期更改保留在重启路径上。不要仅仅为了覆盖其策略字段而将整个 accounts 子树声明为动态。同时包含动态策略和需要重启的设置写入,将保留现有的原子重载和排空行为。

账户作用域重启契约

通道配置更改默认会重启整个通道。多账户通道只有在配置解析读取通道级共享字段以及所选账户、从不读取同级账户,并且 Gateway 能够停止并启动一个 (channel, accountId) 运行时而不替换同级运行时时,才可以设置 reload.accountScopedRestart: true。

作用域路径仅适用于 channels.<channel>.accounts.<non-default-id>.* 下的更改。对共享通道字段、accounts.default、已删除或无法解析的账户,以及可能影响继承的混合更改,会提升为整个通道重启。未选择加入的插件始终使用整个通道路径。

Gateway 会在拆除过程中保留已准入账户的 cfg、已解析的 account 以及所属的 stopAccount 钩子,包括停止失败重试。即使已发布配置删除了该账户或新的插件注册替换了它,清理也必须使用该上下文。

在 stopAccount 的 promise 结算之前,完成其内部的状态更新。Gateway 会忽略在该尝试完成或超时后通过保留的停止回调进行的写入。终止启动状态会使之前的 webhook 交接失效,即使账户 promise 保持待定直到中止;它不会撤销当前任务在恢复后注册入口并显式报告就绪的能力。

依赖账户数量的策略需要整个通道重载。例如,Telegram 会改变空账户 groups 映射在单账户和多账户配置之间继承默认值的方式。Synology Chat 还会跨账户验证继承的和重复的 webhook 路径。这些插件不会选择仅账户重载。

对于使用持久入口排空的通道,账户监视器的停止路径必须先结算所有已接受的传输准入,然后处置并等待其排空。启动账户会打开相同的以账户为键的队列,其初始排空会恢复未分发的持久行。不要添加第二个重载特定的重放路径;队列恢复是规范的重启路径。

当替换一个已知传输身份,且其事件 ID 可能与前一个身份重叠时,在重置其传输游标之前,等待核心提供的队列的 purge()。该操作在一个事务中删除该队列所属通道和账户的所有待处理、已声明、已完成和失败行,并返回删除的行数。在调用之前停止账户的生产者和排空。在 purge 提交之前保留前一个身份标记,以便中断的重置在启动时再次被检测到。相同身份的重启和令牌轮换会保留队列,没有已知前一个身份的旧偏移量也是如此。为了与插件提供的队列兼容,purge 在结构化队列类型上是可选的;需要身份重置的调用方必须失败,而不是跳过不可用的 purge。运行时提供的 purge 句柄会在 worker 准入和提交之前重新检查插件生命周期权限,并在其插件运行时退役后拒绝。账户监视器将其 signal 传递给 purge({ signal }),以便在提交前授权的取消保留这些行。已授权的提交仍会结算。它们还必须在重置游标之前检查取消,如果取消中断了重置,则保留旧身份。

将此标志视为能力声明,而非性能偏好。契约测试应证明:添加和编辑一个命名账户不会改变同级账户的已解析配置;停止一个账户只会结算该账户的 monitor 和 drain;新的 monitor 恰好一次恢复该账户的行。如果任何保证无法证明,则省略该标志。

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