生命周期
安装时检查、Gateway 服务生命周期以及安全的外部 cron 投影。属于 插件钩子 指南的一部分。
安装钩子¶
使用 security.installPolicy 进行由操作员拥有的允许/警告/阻止决策。该策略从 OpenClaw 配置中运行,覆盖 CLI 安装和更新路径,并在启用但不可用时失败关闭。
before_install 是插件运行时生命周期钩子。它可以在插件钩子已经加载的进程中,在 security.installPolicy 之后运行,例如由 Gateway 支持的安装流程。受信任的官方和捆绑安装路径可以跳过此钩子;它们仍会运行操作员安装策略。它适用于插件拥有的观察、警告和兼容性检查,但不是安装的主要企业或主机安全边界。builtinScan 字段仍保留在事件负载中以保持兼容性,但 OpenClaw 不再运行内置的安装时危险代码阻止,因此它是一个空的 ok 结果。返回额外发现或 { block: true, blockReason } 以在该进程中停止安装。
block: true 是终止性的。block: false 被视为无决策。处理程序失败会以失败关闭方式阻止安装。
Gateway 生命周期¶
使用 gateway_start 启动常规插件服务,使用 gateway_stop 清理长期运行的资源。当 gateway_start 运行时,cron 调度器可能仍在加载,因此不要将其用作外部 cron 投影的基线信号。
Gateway 宿主会向 gateway_start 传递可选的 ctx.abortSignal。当重启排空或 Gateway 关闭开始时,它会中止,且发生在关闭加入已接纳的工作之前。使用它来停止周期性调度,并防止待处理回调重新武装其计时器。在开始工作之前检查已中止的信号,并在服务停止或重新加载时移除监听器。将共享资源处置保留在 gateway_stop 或服务的 stop() 方法中,以便已接纳的工作在稳定前保留其依赖项。插件替换和恢复启动钩子接收相同的 Gateway 排空生命周期;仅插件替换不会中止它。
旧版 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 为 Gateway 拥有的 cron 生命周期事件触发,其类型化事件负载涵盖 added、updated、removed、started、finished 和 scheduled 原因。该事件可以包含一个 PluginHookGatewayCronJob 快照(在存在时包括 state.nextRunAtMs、state.lastRunStatus 和 state.lastError),外加一个可选的 PluginHookGatewayCronDeliveryStatus,取值为 not-requested | delivered | not-delivered | unknown。移除事件是提交后的:它们仅在持久删除成功后触发,并且仍携带已删除任务的快照,以便外部调度器协调状态。
scheduled 事件是提交后的:它仅在成功的持久写入更改现有任务的有效 nextRunAtMs 后触发,不包括该任务显式的 added、updated 或 removed 生命周期事件。顶层 event.nextRunAtMs 是已提交的下次唤醒;当它不存在时,任务没有下次唤醒。将这些事件视为协调提示,而不是有序增量日志。将它们用作可合并的提示,以重新读取由 cron_reconciled 最后捕获的调度器;不要从 cron_changed 上下文中采用调度器。保持 OpenClaw 作为到期检查和执行的权威来源。
安全的外部 cron 投影¶
投影完整的唤醒快照,而不是转发 cron 事件增量。外部适配器的 replaceAll 操作必须是原子且幂等的,并且只有在宿主已持久接受快照后才应解决。它还必须尊重提供的中止信号:如果信号在持久接受之前中止,适配器不得接受该快照。
此模式保持一个最新状态工作器在运行。只有 cron_reconciled 采用调度器实例;cron_changed 只是要求该工作器重新读取权威实例,因此迟到的提示无法恢复旧调度器。较新的修订版本会在活动宿主尝试接受过期快照之前中止它。
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([]) 并清除过期的外部唤醒。本示例中的重试/退避
是进程本地的,并将运行时适配器故障视为瞬态故障;请在注册前校验
不可重试的配置。OpenClaw 不为插件钩子副作用提供发件箱。如果进程在持久化确认之前退出,
下一次 Gateway 启动会发出新的权威 cron_reconciled 快照。
gateway_stop 会中止进行中的主机工作,等待工作线程稳定,然后
关闭适配器。
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 cl/openclaw