OpenClaw源码解读系列:配置系统
今天继续OpenClaw源码解读——配置系统。

使用的main分支,commit版本是:f5160ca6becaeeb6a4dfd892fffd2130a696f766
讲解计划如下:
1. CLI 框架与进程模型
2. 配置系统(今日讲解)
3. Gateway 核心
4. 通道与路由
5. Agent 引擎
6. 自动回复管线
7. 插件系统
8. 记忆系统
9. Web 控制台
10. 原生客户端
11. 浏览器自动化
12. 运维与测试
概述
OpenClaw 的配置系统管理着整个应用的行为:模型选择、通道连接、网关设置、Agent 参数等。它基于一个 JSON5 文件,经过一条精心设计的七步处理管线,从磁盘原始字节变成内存中类型安全的 `OpenClawConfig` 对象。

一、配置文件在哪里
配置路径解析在 `src/config/paths.ts` 中实现。核心逻辑是一个优先级搜索:
// src/config/paths.tsexport function resolveStateDir(env: NodeJS.ProcessEnv = process.env,homedir: () => string = envHomedir(env),): string {const effectiveHomedir = () => resolveRequiredHomeDir(env, homedir);const override = env.OPENCLAW_STATE_DIR?.trim() || env.CLAWDBOT_STATE_DIR?.trim();if (override) {return resolveUserPath(override, env, effectiveHomedir);}const newDir = newStateDir(effectiveHomedir);const legacyDirs = legacyStateDirs(effectiveHomedir);const hasNew = fs.existsSync(newDir);if (hasNew) {return newDir;}const existingLegacy = legacyDirs.find((dir) => {try {return fs.existsSync(dir);} catch {return false;}});if (existingLegacy) {return existingLegacy;}return newDir;}
解析优先级:
1. 环境变量 `OPENCLAW_STATE_DIR` / `CLAWDBOT_STATE_DIR` — 显式覆盖
2. `~/.openclaw/` — 新标准目录
3. `~/.clawdbot/` / `~/.moltbot/` / `~/.moldbot/` — 三代旧目录名(品牌重命名历史)
4. 如果都不存在,用 `~/.openclaw/`
配置文件本身也有类似的候选搜索:`openclaw.json` → `clawdbot.json` → `moltbot.json`。这样无论用户从哪个历史版本升级,都能找到已有配置。
二、架构入口:`createConfigIO`
`src/config/io.ts` 是配置系统的核心。`createConfigIO` 是一个工厂函数,返回三个方法:
// src/config/io.tsreturn {configPath,loadConfig,readConfigFileSnapshot,writeConfigFile,};
而全局导出的 `loadConfig()` 只是一个带缓存的便捷包装:
// src/config/io.tsexport function loadConfig(): OpenClawConfig {const io = createConfigIO();const configPath = io.configPath;const now = Date.now();if (shouldUseConfigCache(process.env)) {const cached = configCache;if (cached && cached.configPath === configPath && cached.expiresAt > now) {return cached.config;}}const config = io.loadConfig();// ... cache update ...return config;}
缓存 TTL 默认 200ms(`OPENCLAW_CONFIG_CACHE_MS` 可调),避免同一事件循环内反复读磁盘。Gateway 处理一次请求可能多次调用 `loadConfig()`,缓存保证只读一次。
三、七步处理管线:`loadConfig` 内部
`createConfigIO` 内部的 `loadConfig()` 方法(第 272-382 行)实现了完整管线。以用户配置文件 `~/.openclaw/openclaw.json` 为例:
第 1 步:读取并解析 JSON5
const raw = deps.fs.readFileSync(configPath, "utf-8");const parsed = deps.json5.parse(raw);
使用 JSON5(而非标准 JSON),支持注释、尾逗号、单引号等,对手动编辑非常友好。
第 2 步:`$include` 指令展开
const resolved = resolveConfigIncludes(parsed, configPath, {readFile: (p) => deps.fs.readFileSync(p, "utf-8"),parseJson: (raw) => deps.json5.parse(raw),});
配置文件中可以写 `{ “$include”: “./channels.json” }` 来拆分大配置。`resolveConfigIncludes` 递归展开所有 include,检测循环引用(`CircularIncludeError`)。
第 3 步:配置内环境变量导出
if (resolved && typeof resolved === "object" && "env" in resolved) {applyConfigEnv(resolved as OpenClawConfig, deps.env);}
配置文件中的 `env.vars` 字段(如 `{ “env”: { “vars”: { “MY_KEY”: “xxx” } } }`)会被注入到 `process.env`,但不覆盖已有值。这在第 4 步之前执行,确保 `${MY_KEY}` 能正确引用。
第 4 步:`${VAR}` 环境变量替换
const substituted = resolveConfigEnvVars(resolved, deps.env);
递归遍历所有字符串值,把 `${TELEGRAM_BOT_TOKEN}` 替换为 `process.env.TELEGRAM_BOT_TOKEN` 的实际值。若变量不存在,抛出 `MissingEnvVarError`。这让敏感信息(API Key、Token)不必硬写进配置文件。
第 5 步:Zod 校验
const validated = validateConfigObjectWithPlugins(resolvedConfig);if (!validated.ok) {// ... 打印错误 ...throw error;}
`validateConfigObjectWithPlugins` 内部使用 `zod-schema.ts` 定义的 `OpenClawSchema` 对整个配置做结构校验。Zod schema 与 TypeScript 类型一一对应,确保运行时数据和编译时类型一致。校验还支持插件扩展:每个插件可以注册额外的 schema 分支。
第 6 步:默认值注入
const cfg = applyModelDefaults(applyCompactionDefaults(applyContextPruningDefaults(applyAgentDefaults(applySessionDefaults(applyLoggingDefaults(applyMessageDefaults(validated.config))),),),),);
七层 `apply*Defaults` 函数从内到外嵌套执行,为未设置的字段填充默认值。例如 `applyModelDefaults` 会补充模型别名(`opus` → `anthropic/claude-opus-4-6`、`gemini` → `google/gemini-3-pro-preview`)和默认 cost/input/maxTokens。
这里有一个重要设计:默认值只在 `loadConfig` 时注入,不写入磁盘。用户配置文件永远只保存显式设置的值。
第 7 步:路径规范化 + 运行时覆盖
normalizeConfigPaths(cfg);// ...return applyConfigOverrides(cfg);
-
`normalizeConfigPaths` 把配置中的 `~/` 路径展开为绝对路径
-
`applyConfigOverrides` 应用通过 `setConfigOverride()` 设置的运行时覆盖(测试或 dev 模式下临时改某个值,不影响磁盘配置)
四、配置类型系统
根类型在 `src/config/types.openclaw.ts`:
export type OpenClawConfig = {meta?: {lastTouchedVersion?: string;lastTouchedAt?: string;};auth?: AuthConfig;env?: { ... };wizard?: { ... };diagnostics?: DiagnosticsConfig;logging?: LoggingConfig;// ...};
`OpenClawConfig` 由 30+ 个细分类型组成,每个单独定义在 `types.*.ts` 文件中:
-
`types.agents.ts`:Agent 列表、绑定、默认参数
-
`types.gateway.ts`:端口、绑定地址、TLS、发现、Control UI
-
`types.channels.ts`:通道全局默认(心跳可见性等)
-
`types.models.ts`:模型定义、别名、允许列表
-
`types.auth.ts`:Auth profile(OAuth、API Key)
-
`types.sandbox.ts`:沙箱配置(Docker、browser)
-
`types.tools.ts`:工具权限与配置
-
`types.memory.ts`:记忆系统(embedding、后端)
对应的 Zod schema 在 `zod-schema.*.ts` 中,与类型文件一一映射。
五、写入配置:Merge Patch + 原子写
当 `openclaw config set gateway.port 8080` 或 onboarding wizard 修改配置时,调用 `writeConfigFile`:
// src/config/io.tsasync function writeConfigFile(cfg: OpenClawConfig) {clearConfigCache();let persistCandidate: unknown = cfg;const snapshot = await readConfigFileSnapshot();if (snapshot.valid && snapshot.exists) {const patch = createMergePatch(snapshot.config, cfg);persistCandidate = applyMergePatch(snapshot.resolved, patch);}// ... validate ...const json = JSON.stringify(stampConfigVersion(validated.config), null, 2).trimEnd().concat("\n");const tmp = path.join(dir,`${path.basename(configPath)}.${process.pid}.${crypto.randomUUID()}.tmp`,);await deps.fs.promises.writeFile(tmp, json, { encoding: "utf-8", mode: 0o600 });// ... rotate backups ...await deps.fs.promises.rename(tmp, configPath);}
三个关键设计:
1. Merge Patch 而非全量覆盖:`createMergePatch(snapshot.config, cfg)` 计算出差异,再 `applyMergePatch(snapshot.resolved, patch)` 应用到 `resolved`(include 展开 + env 替换后、但未注入默认值的版本)。这确保只修改用户实际改动的字段,不会把运行时默认值写进配置文件。
2. 原子写入:先写到 `.tmp` 临时文件(`mode: 0o600`,仅 owner 可读),再 `rename` 覆盖。`rename` 在 POSIX 系统上是原子操作,不会出现写一半断电导致配置损坏。Windows 上会 fallback 到 `copyFile`。
3. 备份轮转:写入前把旧配置备份为 `.bak`,并维护最多 5 个历史版本(`.bak.1` 到 `.bak.4`),类似 logrotate。
六、`readConfigFileSnapshot` 与配置守卫
`readConfigFileSnapshot` 是 `loadConfig` 的”诊断版本”——不抛异常,而是返回一个 `ConfigFileSnapshot` 对象,包含 `valid`、`issues`、`legacyIssues` 等诊断信息。它被两处关键代码使用:
1. preAction 中的 `ensureConfigReady`:每个 CLI 命令执行前校验配置,无效则阻止执行并提示 `openclaw doctor –fix`
2. Gateway 启动时的 `startGatewayServer`:启动前读快照,若有 legacy 问题则尝试自动迁移(`migrateLegacyConfig`)
`snapshot.resolved`(include/env 展开后但无默认值)和 `snapshot.config`(完整含默认值)刻意分开存储,因为 `config set` 写回时需要基于 `resolved` 做 merge patch,而 `gateway run` 用的是 `config`。
七、要点回顾
|
设计点 |
解决的问题 |
实现 |
|---|---|---|
|
JSON5 |
手动编辑体验差 |
`json5.parse`,支持注释和尾逗号 |
|
`$include` |
大配置文件拆分 |
递归展开 + 循环检测 |
|
`${VAR}` |
敏感信息不入库 |
递归字符串替换,缺失则报错 |
|
Zod 校验 |
运行时类型安全 |
schema 与 TS 类型一一对应 |
|
默认值不写磁盘 |
配置文件干净 |
`loadConfig` 注入,`writeConfigFile` 用 resolved |
|
Merge Patch |
局部修改不丢字段 |
diff → patch → 写入 resolved |
|
原子写入 + 备份 |
断电不损坏 |
tmp + rename + .bak 轮转 |
|
200ms 缓存 |
同一请求内重复读 |
TTL 缓存,写入时清空 |
|
多代路径搜索 |
品牌重命名兼容 |
openclaw → clawdbot → moltbot 候选 |
掌握这套管线后,你就能理解配置从编辑到生效的每一步,也能安全地在代码中修改配置逻辑。
夜雨聆风
