23.3 配置热重载

生成模型:Claude Opus 4.6 (anthropic/claude-opus-4-6) Token 消耗:输入 ~200k tokens,输出 ~8k tokens(本节)


生产环境中,修改配置后不得不重启整个服务是一件代价高昂的事——正在进行的对话会中断,WebSocket 连接会断开,所有状态被清洗。OpenClaw 的配置热重载系统通过"文件监听 → 差异比对 → 规则路由 → 分级执行"四个阶段,让大多数配置变更可以在不中断服务的前提下即时生效。本节剖析这套机制的完整实现。


23.3.1 配置重载机制(src/gateway/config-reload.ts

整体架构

配置热重载的核心流程可以概括为一条管道:

文件变更 → chokidar 监听 → debounce(300ms) → 读取快照 → diffConfigPaths
→ buildGatewayReloadPlan → 模式判定(off/restart/hot/hybrid) → 执行重载

让我们逐步拆解每个环节。

文件监听:chokidar

OpenClaw 使用 chokidar 库来监视配置文件的变化。startGatewayConfigReloader 在启动时创建一个 watcher:

// src/gateway/config-reload.ts(简化)

const watcher = chokidar.watch(opts.watchPath, {
  ignoreInitial: true,                      // 不触发初始扫描事件
  awaitWriteFinish: {
    stabilityThreshold: 200,                 // 文件写入稳定后 200ms 才认为完成
    pollInterval: 50,                        // 轮询间隔 50ms
  },
  usePolling: Boolean(process.env.VITEST),   // 测试环境使用轮询模式
});

watcher.on("add", schedule);     // 新文件
watcher.on("change", schedule);  // 文件修改
watcher.on("unlink", schedule);  // 文件删除

衍生解释chokidar 是 Node.js 生态中最流行的文件监听库,它封装了各操作系统的原生文件系统事件(Linux 的 inotify、macOS 的 FSEvents、Windows 的 ReadDirectoryChangesW),提供统一的跨平台 API。awaitWriteFinish 选项解决了一个常见问题:编辑器保存文件时可能分多次写入(先清空再写入内容),如果不等待写入稳定,可能会读到一个半截的文件。

防抖(Debounce)

收到文件变更事件后,不会立即执行重载,而是通过防抖延迟 300ms:

衍生解释:**防抖(Debounce)是一种控制函数调用频率的技术。当事件连续触发时,只在最后一次触发后等待一段时间(此处为 300ms)才执行回调。这与节流(Throttle)**不同——节流保证固定时间间隔内至少执行一次,而防抖保证"安静"一段时间后才执行。在配置热重载场景中,用户可能连续保存多次文件,防抖确保只有最后一次保存才触发重载逻辑。

差异比对:diffConfigPaths

重载流程的核心是比较"上一份配置"和"新配置"之间有哪些路径发生了变化。diffConfigPaths 递归遍历两个配置对象,返回所有值不同的路径:

例如,如果用户把 cron.schedule"*/5 * * * *" 改成了 "*/10 * * * *"diffConfigPaths 会返回 ["cron.schedule"]

规则路由:buildGatewayReloadPlan

配置变更路径得到后,下一步是决定"怎么重载"。OpenClaw 维护了一个有优先级的规则表——每个配置路径前缀对应三种处理方式之一:

类别
含义
执行方式

"restart"

必须完全重启 Gateway

发送 SIGUSR1 信号触发进程重启

"hot"

可以热重载

仅重启受影响的子系统

"none"

无需重载

忽略(下次读取时自然生效)

规则表分为三层:基础规则(头部)、渠道插件规则(动态)、基础规则(尾部):

插件规则注入:渠道插件(Channel Plugin)可以通过 plugin.reload.configPrefixesplugin.reload.noopPrefixes 声明自己的重载规则。这些规则插入到头部和尾部之间:

设计要点:规则匹配采用**最先匹配(first match)**策略——matchRule 线性遍历规则表,返回第一个前缀匹配的规则。这意味着头部规则的优先级高于尾部。例如 hooks.gmail 的规则在 hooks 之前,所以 Gmail 钩子的变更会触发 restart-gmail-watcher 而非通用的 reload-hooks

构建重载计划

buildGatewayReloadPlan 遍历所有变更路径,根据匹配到的规则构建一个详细的重载计划:

注意一个安全性设计:如果某个变更路径未匹配到任何规则(rule === null),系统默认将其视为 "restart"——宁可多重启也不遗漏关键配置变更。

四种重载模式

重载计划构建后,根据 gateway.reload.mode 配置决定如何执行:

模式
行为

"off"

完全禁用热重载,忽略一切文件变更

"restart"

不管变更内容,一律重启 Gateway

"hot"

仅执行热重载部分;需要重启的变更被忽略(日志警告)

"hybrid"(默认)

优先热重载;如果有需要重启的变更,则触发完整重启

并发保护与重入防护

热重载过程中需要防止并发执行(例如用户在重载进行时又保存了文件):

这是一个经典的互斥锁(mutex)+排队模式:running 标志确保同一时间只有一个重载在执行;pending 标志确保在重载期间到达的新变更不会丢失,而是在当前重载完成后重新触发。


23.3.2 运行时覆盖(src/config/runtime-overrides.ts

除了文件级别的配置修改,OpenClaw 还支持在运行时通过 API 临时覆盖配置值——这些覆盖存储在内存中,不会写入磁盘,进程重启后失效。

覆盖树

运行时覆盖使用一个模块级变量 overrides 存储,它的结构与 OpenClawConfig 相同,但只包含被覆盖的路径:

设置与取消覆盖

setConfigOverrideunsetConfigOverride 通过配置路径语法操作覆盖树:

路径解析使用 parseConfigPath(见 23.1 节),支持点分和方括号语法(如 agents.list[0].model.primary),并内置了原型链污染防护。

深度合并:覆盖如何生效

applyConfigOverrides 在配置加载流水线的最后一步被调用,将覆盖树深度合并到配置对象上:

合并语义如下:

基础值
覆盖值
结果

对象

对象

递归合并(保留基础值中未被覆盖的键)

对象

非对象

覆盖值替换

非对象

任意

覆盖值替换

任意

undefined

保持基础值

使用场景:运行时覆盖通常由 Web 控制台的"临时调试"功能触发,例如临时切换模型、临时调高日志级别等,无需修改配置文件。


23.3.3 服务器重载处理器(src/gateway/server-reload-handlers.ts

热重载计划构建完成后,实际执行重载的工作由 createGatewayReloadHandlers 工厂函数创建的两个处理器完成:applyHotReload(热重载)和 requestGatewayRestart(完整重启)。

工厂模式与状态注入

createGatewayReloadHandlers 接收一组依赖和状态访问器,返回两个处理函数。这种设计将重载逻辑与 Gateway 的具体状态解耦:

applyHotReload:分子系统重载

applyHotReload 根据重载计划中的标志位,逐个重启受影响的子系统:

注意几个设计要点:

  1. stop-then-start 模式:定时任务、浏览器控制、Gmail 监听器都采用"先停止旧实例,再启动新实例"的方式,避免资源泄漏。

  2. 环境变量跳过OPENCLAW_SKIP_GMAIL_WATCHEROPENCLAW_SKIP_CHANNELS 环境变量允许在开发/调试时跳过某些子系统的重启。

  3. 并发度即时更新:命令队列的并发度(主 Agent、子 Agent、Cron 任务各自的并发上限)在每次热重载时同步更新。

  4. 目录缓存清除resetDirectoryCache() 确保 Agent 目录解析不会使用过时的缓存。

requestGatewayRestart:完整重启

当变更无法热重载时,系统通过 Unix 信号触发 Gateway 进程重启:

衍生解释SIGUSR1 是 POSIX 标准定义的用户自定义信号(User-defined Signal 1)。在 Node.js 中,process.emit("SIGUSR1") 不会终止进程,而是触发注册的监听器。OpenClaw Gateway 在启动时注册了一个 SIGUSR1 监听器来执行优雅重启(graceful restart)——先停止接受新请求,等待进行中的请求完成,然后重新初始化所有子系统。authorizeGatewaySigusr1Restart() 设置一个授权标志,防止外部进程的 SIGUSR1 信号意外触发重启。

热重载的完整数据流

将三个子节的内容综合起来,整个配置热重载的数据流如下:


本节小结

  1. 文件监听使用 chokidar,配合 awaitWriteFinish 等待文件写入稳定,避免读取不完整的配置。

  2. 防抖机制(默认 300ms)避免连续保存触发多次重载,同时互斥锁+排队保证不会并发执行重载。

  3. 差异比对diffConfigPaths)递归比较新旧配置,返回所有变更的路径列表。

  4. 规则路由将每个变更路径映射到 restart/hot/none 三种处理方式,规则表支持渠道插件动态注入。

  5. 四种重载模式——off(禁用)、restart(总是重启)、hot(仅热重载)、hybrid(混合,默认)——提供了灵活的策略选择。

  6. 运行时覆盖提供纯内存的配置覆盖机制,通过深度合并叠加在磁盘配置之上,适用于临时调试。

  7. 热重载处理器按子系统分别执行 stop-then-start,确保每个子系统独立重载且不泄漏资源。

  8. 完整重启通过 SIGUSR1 信号触发优雅重启,带有授权机制防止意外触发。

Last updated