本页内容

本页内容

构建插件

网关并安装生命周期挂钩

安装时检查、网关服务生命周期和安全的外部 cron 投影。 插件挂钩 指南的一部分。

安装钩子

使用 security.installPolicy 进行运营商拥有的 allow/warn/block 决策。该策略从 OpenClaw 配置运行,涵盖 CLI 安装和更新路径,并在启用但不可用时阻止安装。

before_install 是一个插件运行时生命周期挂钩。它可以在已加载插件挂钩的进程中在 security.installPolicy 之后运行,例如网关支持的安装流程。受信任的官方和捆绑安装路径可以跳过此钩子;他们仍然运行运营商安装政策。它对于插件拥有的观察、警告和兼容性检查很有用,但它不是安装的主要企业或主机安全边界。为了兼容性,builtinScan 字段保留在事件负载中,但 OpenClaw 不再运行内置的安装时危险代码阻止,因此它是空的 ok 结果。返回其他结果或 { block: true, blockReason } 以停止该过程中的安装。

block: true 是终端。 block: false 被视为没有决定。处理程序失败会阻止安装。

网关生命周期

使用 gateway_start 来启动通用插件服务,并使用 gateway_stop 来清理长期运行的资源。cron 调度器在 gateway_start 运行时仍可能处于加载中,因此不要把它作为外部 cron 投影的基线信号。

网关主机将可选的 ctx.abortSignal 传递给 gateway_start。当重新启动耗尽或网关关闭开始时,在关闭加入承认的工作之前,它会中止。使用它来停止定期调度并防止待处理的回调重新配置其计时器。在开始工作之前检查已经中止的信号,并在服务停止或重新加载时删除侦听器。将共享资源处置保留在 gateway_stop 或服务的 stop() 方法中,以便承认的工作保留其依赖性,直到解决为止。插件替换和恢复启动挂钩具有相同的网关耗尽生命周期;仅插件替换不会中止它。

旧版 api.on("deactivate", ...) 别名已于 2026 年 8 月删除。使用 gateway_stop 进行清理;请参阅 迁移说明 。

不要依赖内部的 gateway:startup 钩子来运行插件拥有的运行时服务。

cron_reconciled 会在 Gateway 的 cron 调度器及其退出时监听器完成有状态协调后触发。它既会在初始启动时触发,也会在配置重载时调度器替换后触发。该事件会报告 reason(startup 或 reload)以及实际生效的 enabled 状态。即使 cron 被禁用,也会以 enabled: false 触发,从而允许外部投影清除过期的唤醒。使用 ctx.getCron?.() 获取完成协调的精确调度器实例;之后的重载不会重新指向该回调。ctx.abortSignal 持有同一份调度器快照。Gateway 会在有更新的调度器被启用或关闭开始时立刻中止它。请将它传递给每一个持久化副作用,并且在它中止后不要再接受该快照。 这是一个调度器生命周期信号,不是插件激活信号:仅插件热重载不会再次触发它。新启用的消费者会在下一次调度器替换或 Gateway 启动时收到它的第一个基线信号。

与其他观察钩子类似,gateway_start 和 cron_reconciled 的回调可能会重叠。如果两个处理器共享插件初始化,请使用插件本地的就绪 promise 来协调,而不要依赖回调顺序。

cron_changed 针对网关拥有的 cron 生命周期事件触发,其类型化事件负载涵盖 added、updated、removed、started、finished 和 scheduled 原因。该事件可以包括 PluginHookGatewayCronJob 快照(包括 state.nextRunAtMs、state.lastRunStatus 和 state.lastError(如果存在))以及 not-requested | delivered | 的可选 PluginHookGatewayCronDeliveryStatus not-delivered | unknown。删除的事件是提交后的:它们仅在持久删除成功后触发,并且仍然携带已删除的作业快照,以便外部调度程序可以协调状态。

scheduled 事件也属于提交后事件:它只会在一次成功的持久化写入改变了现有作业的有效 nextRunAtMs 之后触发,并且不包括该作业显式的 added、updated 或 removed 生命周期事件。顶层的 event.nextRunAtMs 是已提交的下一次唤醒时间;当它缺失时,表示该作业没有下一次唤醒。请把这些事件视为协调提示,而不是有序的增量日志。将它们作为可合并的提示,用来重新读取由 cron_reconciled 最后捕获的调度器;不要从 cron_changed 上下文中接管调度器。将 OpenClaw 作为到期检查和执行的唯一真实来源。

安全的外部 cron 投影

投影完整的唤醒快照,而不是转发 cron 事件增量。外部适配器的 replaceAll 操作必须是原子的并且可以安全地重复,并且必须仅在主机持久接受快照后才能解析。它还必须遵守提供的中止信号:如果信号在持久接受之前中止,则适配器不得接受该快照。

这种模式使得同一时刻只有一个最新状态 worker 在运行。只有 cron_reconciled 会接管一个调度器实例;cron_changed 只是要求该 worker 重新读取权威实例,因此迟到的提示不会恢复较旧的调度器。更新的版本会在宿主尝试接受陈旧快照之前中止当前尝试。

typescript
import { setTimeout as sleep } from "node:timers/promises";import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"; type ExternalWake = { jobId: string; runAtMs: number }; type ExternalWakeHost = {  replaceAll(wakes: readonly ExternalWake[], options: { signal: AbortSignal }): Promise<void>;  close(): Promise<void>;}; type CronReader = {  list(options: { includeDisabled: true }): Promise<    Array<{      id: string;      enabled?: boolean;      state?: { nextRunAtMs?: number };    }>  >;}; export function registerCronProjection(api: OpenClawPluginApi, host: ExternalWakeHost) {  const lifecycle = new AbortController();  let cron: CronReader | undefined;  let enabled = false;  let hasBaseline = false;  let reconciliationSignal: AbortSignal | undefined;  let requestedRevision = 0;  let appliedRevision = 0;  let worker = Promise.resolve();  let activeAttempt: AbortController | undefined;   const projectLatest = async () => {    let retryMs = 1_000;     while (!lifecycle.signal.aborted && appliedRevision < requestedRevision) {      const ownerSignal = reconciliationSignal;      if (!ownerSignal || ownerSignal.aborted) {        return;      }      const targetRevision = requestedRevision;      const attempt = new AbortController();      const signal = AbortSignal.any([lifecycle.signal, ownerSignal, attempt.signal]);      activeAttempt = attempt;       try {        const jobs = enabled && cron ? await cron.list({ includeDisabled: true }) : [];        if (signal.aborted || targetRevision !== requestedRevision) {          continue;        }        const wakes = jobs          .flatMap((job): ExternalWake[] => {            const runAtMs = job.enabled === false ? undefined : job.state?.nextRunAtMs;            return runAtMs === undefined ? [] : [{ jobId: job.id, runAtMs }];          })          .sort((a, b) => a.runAtMs - b.runAtMs || a.jobId.localeCompare(b.jobId));         await host.replaceAll(wakes, { signal });        if (signal.aborted || targetRevision !== requestedRevision) {          continue;        }        appliedRevision = targetRevision;        retryMs = 1_000;      } catch {        if (lifecycle.signal.aborted || ownerSignal.aborted) {          return;        }        if (attempt.signal.aborted) {          continue;        }        api.logger.warn(`external cron projection failed; retrying in ${retryMs}ms`);        try {          await sleep(retryMs, undefined, { signal });        } catch {          if (lifecycle.signal.aborted) {            return;          }          if (attempt.signal.aborted) {            continue;          }        }        retryMs = Math.min(retryMs * 2, 30_000);      } finally {        if (activeAttempt === attempt) {          activeAttempt = undefined;        }      }    }  };   const requestProjection = () => {    const targetRevision = ++requestedRevision;    activeAttempt?.abort();    worker = worker.then(async () => {      if (!lifecycle.signal.aborted && appliedRevision < targetRevision) {        await projectLatest();      }    });    return worker;  };   api.on("cron_reconciled", (event, ctx) => {    const reconciledCron = ctx.getCron?.();    if (event.enabled && !reconciledCron) {      api.logger.warn("cron reconciliation did not expose a scheduler");      return;    }    cron = reconciledCron;    enabled = event.enabled;    hasBaseline = true;    reconciliationSignal = ctx.abortSignal;    return requestProjection();  });   api.on("cron_changed", () => {    if (hasBaseline) {      return requestProjection();    }  });   api.on("gateway_stop", async () => {    lifecycle.abort();    await worker;    await host.close();  });}

当 cron_reconciled 报告 enabled: false 时,同一路径调用 replaceAll([]) 并清除过时的外部唤醒。本示例中的 Retry/backoff 是进程本地的,并将运行时适配器故障视为暂时的;在注册之前验证不可重试的配置。 OpenClaw 不提供插件挂钩效果的发件箱。如果进程在持久接受之前退出,则下一次网关启动会发出新的权威 cron_reconciled 快照。 gateway_stop 中止正在进行的主机工作,等待工作线程稳定下来,然后关闭适配器。

本页有帮助吗?