April 16, 2026 • 版本: 2026.3.23-2

openclaw status 命令在文件支持的 SecretRef 配置上因插件加载重新读取未解析的原始配置而失败

当通道密钥迁移到文件支持的 SecretRef 时,openclaw status 命令在插件注册期间崩溃,因为 ensurePluginRegistryLoaded() 重新读取了未解析的原始配置,而不是使用网关运行时中已经解析的配置。

🔍 症状

主要错误表现

当对使用文件后端 SecretRef 的 channel secrets 配置执行 openclaw status 时,CLI 在插件注册期间崩溃:

$ openclaw status
[plugins] feishu failed during register ... Error: channels.feishu.appSecret: unresolved SecretRef "file:localfile:/channels/feishu/appSecret". Resolve this command against an active gateway runtime snapshot before reading it.
[openclaw] Failed to start CLI: PluginLoadFailureError: plugin load failed: feishu ...

成功执行的并行命令

有趣的是,同一环境中的相关命令执行成功:

$ openclaw health --json
{"status":"healthy","gateway":"connected","timestamp":"2026-03-23T14:32:15.421Z"}

$ openclaw status --json
{"channels":[{"name":"feishu","status":"active","connected":true}],"gateway":"connected"}

环境上下文

故障在特定条件下发生:

  • 操作系统: Linux `6.17.0-19-generic`
  • Node.js: `v22.22.1`
  • 安装方式: 全局包安装,位于 `~/.npm-global/lib/node_modules/openclaw`
  • OpenClaw 版本: `2026.3.23-2`

SecretRef 配置模式

有问题的配置在 ~/.openclaw/openclaw.json 中使用以下 SecretRef 结构:

{
  "channels": {
    "feishu": {
      "appId": "cli-app-01",
      "appSecret": {
        "source": "file",
        "provider": "localfile",
        "id": "/channels/feishu/appSecret"
      }
    }
  }
}

实际密钥值存储在 ~/.openclaw/secrets.json 中:

{
  "/channels/feishu/appSecret": "fl.1234567890abcdef..."
}

🧠 根因分析

架构概述

该故障源于插件加载和命令级 SecretRef 解析之间的配置生命周期不匹配。CLI 架构有两个相互竞争的配置消费路径:

  1. 命令级解析:`status` 命令通过活动网关运行时快照解析 SecretRef
  2. 路由级插件加载:路由使用原始、未解析的配置预加载插件

问题 1:路由层过早预加载插件

src/cli/program/routes.ts 中,路由配置指定:

// Before (problematic)
loadPlugins: (argv) => !hasFlag(argv, "--json"),

这导致插件在 status 命令之前加载,此时命令处理器还未执行。插件注册阶段尝试访问 channel 凭证,而凭证仍处于未解析的 SecretRef 形式。

问题 2:插件注册表始终读取原始配置

src/cli/plugin-registry.ts 中,ensurePluginRegistryLoaded() 函数无条件调用 loadConfig()

// src/cli/plugin-registry.ts
export async function ensurePluginRegistryLoaded(options?: PluginLoadOptions): Promise {
  if (registryState.loaded) {
    return;
  }
  
  // This ALWAYS re-reads from disk, discarding any resolved config
  const config = loadConfig();  // ← BUG: ignores resolved config passed via options
  
  await loadPlugins(config, options?.scope);
  registryState.loaded = true;
}

函数签名接受 options 参数,但未利用它进行配置覆盖:

interface PluginLoadOptions {
  scope?: "all" | "configured-channels" | "core-only";
  config?: ResolvedConfig;  // ← This parameter exists but is unused
}

问题 3:Status 命令解析发生时机过晚

status 命令在 src/commands/status.ts 中正确地通过网关解析 SecretRef:

// src/commands/status.ts (simplified)
export async function statusCommand(argv: StatusArgs): Promise {
  // Step 1: Resolve config via gateway (this works correctly)
  const cfg = await resolveCommandSecretRefsViaGateway(rawConfig, gateway);
  
  // Step 2: Load plugins (CRASHES HERE - cfg is not passed)
  ensurePluginRegistryLoaded({ scope: "configured-channels" });  // ← Missing config: cfg
  
  // Step 3: Display status (never reached)
  await renderStatus(cfg);
}

执行时间线对比

阶段openclaw health --jsonopenclaw status
路由插件预加载跳过(有 --json触发崩溃
命令处理器执行解析配置解析配置
插件注册使用已解析配置使用未解析配置

为什么 --json 标志有效

--json 标志绕过插件预加载,因为路由配置有条件地跳过插件加载:

loadPlugins: (argv) => !hasFlag(argv, "--json"),

使用 --json 时,插件加载会延迟到命令处理器解析配置之后,允许 ensurePluginRegistryLoaded() 接收正确的配置上下文。

🛠️ 逐步修复

阶段 1:修改插件注册表以接受配置覆盖

文件: src/cli/plugin-registry.ts

更改:利用已存在但未使用的 config 选项参数。

// Before
export async function ensurePluginRegistryLoaded(options?: PluginLoadOptions): Promise {
  if (registryState.loaded) {
    return;
  }
  
  const config = loadConfig();  // Always raw config
  
  await loadPlugins(config, options?.scope);
  registryState.loaded = true;
}

// After
export async function ensurePluginRegistryLoaded(options?: PluginLoadOptions): Promise {
  if (registryState.loaded) {
    return;
  }
  
  const config = options?.config ?? loadConfig();  // Use provided resolved config or fallback
  
  await loadPlugins(config, options?.scope);
  registryState.loaded = true;
}

阶段 2:禁用 Status 的路由级插件预加载

文件: src/cli/program/routes.ts

更改:将 loadPlugins 无条件设置为 false

// Before
const statusRoute: RouteDefinition = {
  command: "status",
  describe: "Show gateway and channel status",
  builder: (yargs) => yargs
    .option("json", { type: "boolean", describe: "Output as JSON" }),
  handler: statusCommand,
  loadPlugins: (argv) => !hasFlag(argv, "--json"),  // ← Conditional loading
};

// After
const statusRoute: RouteDefinition = {
  command: "status",
  describe: "Show gateway and channel status",
  builder: (yargs) => yargs
    .option("json", { type: "boolean", describe: "Output as JSON" }),
  handler: statusCommand,
  loadPlugins: false,  // ← Defer to command handler for proper resolution
};

阶段 3:在 Status 命令中传递已解析配置给插件加载

文件: src/commands/status.ts

更改:将已解析的 cfg 传递给 ensurePluginRegistryLoaded()

// Before
export async function statusCommand(argv: StatusArgs): Promise {
  const cfg = await resolveCommandSecretRefsViaGateway(rawConfig, gateway);
  
  ensurePluginRegistryLoaded({ scope: "configured-channels" });  // No config passed
  
  await renderStatus(cfg);
}

// After
export async function statusCommand(argv: StatusArgs): Promise {
  const cfg = await resolveCommandSecretRefsViaGateway(rawConfig, gateway);
  
  ensurePluginRegistryLoaded({ scope: "configured-channels", config: cfg });  // Pass resolved config
  
  await renderStatus(cfg);
}

阶段 4:对 JSON 变体应用相同修复

文件: src/commands/status-json.ts

更改:镜像 status.ts 的修复。

// Before
export async function statusJsonCommand(argv: StatusJsonArgs): Promise {
  const cfg = await resolveCommandSecretRefsViaGateway(rawConfig, gateway);
  
  ensurePluginRegistryLoaded({ scope: "configured-channels" });
  
  const result = await gatherChannelStatus(cfg);
  console.log(JSON.stringify(result, null, 2));
}

// After
export async function statusJsonCommand(argv: StatusJsonArgs): Promise {
  const cfg = await resolveCommandSecretRefsViaGateway(rawConfig, gateway);
  
  ensurePluginRegistryLoaded({ scope: "configured-channels", config: cfg });
  
  const result = await gatherChannelStatus(cfg);
  console.log(JSON.stringify(result, null, 2));
}

完整 Diff 摘要

--- src/cli/program/routes.ts
+++ src/cli/program/routes.ts
@@
- loadPlugins: (argv) => !hasFlag(argv, "--json"),
+ loadPlugins: false,

--- src/cli/plugin-registry.ts
+++ src/cli/plugin-registry.ts
@@
- const config = loadConfig();
+ const config = options?.config ?? loadConfig();

--- src/commands/status.ts
+++ src/commands/status.ts
@@
- ensurePluginRegistryLoaded({ scope: "configured-channels" });
+ ensurePluginRegistryLoaded({ scope: "configured-channels", config: cfg });

--- src/commands/status-json.ts
+++ src/commands/status-json.ts
@@
- ensurePluginRegistryLoaded({ scope: "configured-channels" });
+ ensurePluginRegistryLoaded({ scope: "configured-channels", config: cfg });

🧪 验证

修复前验证(预期:失败)

在应用修复之前,确认失败状态:

$ openclaw status
[plugins] feishu failed during register ... Error: channels.feishu.appSecret: unresolved SecretRef "file:localfile:/channels/feishu/appSecret".
[openclaw] Failed to start CLI: PluginLoadFailureError: plugin load failed: feishu ...

$ echo $?
1

修复后验证(预期:成功)

应用修复后,验证命令成功:

$ openclaw status
Gateway:     connected
Channels:    1 configured, 1 active
├── feishu   ✓ healthy (latency: 142ms)

$ echo $?
0

JSON 输出验证

$ openclaw status --json
{
  "gateway": {
    "status": "connected",
    "version": "2026.3.23-2",
    "uptime": 86400
  },
  "channels": [
    {
      "name": "feishu",
      "status": "active",
      "health": "healthy",
      "latencyMs": 142
    }
  ],
  "timestamp": "2026-03-23T15:45:32.001Z"
}

$ echo $?
0

Secret 诊断验证

执行诊断命令以确认 SecretRef 解析正常:

$ openclaw diagnostics secret-refs
{"diagnostics":[],"summary":{"total":1,"resolved":1,"unresolved":0}}

$ openclaw secret-refs --verify
SecretRef verification complete.
All 1 SecretRef(s) resolved successfully.

并行健康检查(回归预防)

验证修复不会破坏现有的 --json 变体:

$ openclaw health --json
{"status":"healthy","gateway":"connected","timestamp":"2026-03-23T15:45:32.100Z"}

$ echo $?
0

插件加载顺序验证

通过检查调试输出确认插件在配置解析后加载:

$ OPENCLAW_DEBUG=plugin-load openclaw status 2>&1 | head -20
[plugins] Initializing plugin registry...
[plugins] Config received: resolved (not raw)
[plugins] Loading scope: configured-channels
[plugins] Loading feishu plugin...
[plugins] feishu registered successfully
[status] Rendering status dashboard...

网关运行时快照验证

如果可用,验证网关的已解析配置快照是否符合预期:

$ openclaw gateway config-snapshot --format=json | jq '.channels.feishu.appSecret'
{
  "source": "file",
  "provider": "localfile",
  "id": "/channels/feishu/appSecret"
}

$ openclaw gateway config-snapshot --resolved --format=json | jq '.channels.feishu.appSecret'
"fl.1234567890abcdef..."  # Actual resolved value

⚠️ 常见陷阱

陷阱 1:使用缓存注册表进行多次插件注册

问题:第一次调用 ensurePluginRegistryLoaded() 后,注册表被标记为已加载。后续使用不同配置参数的调用将被忽略。

症状

$ openclaw status  # Works
$ openclaw health   # Uses cached (stale) registry

缓解措施:确保需要不同插件范围 的命令在 ensurePluginRegistryLoaded() 之前调用 resetPluginRegistry(),或在配置重大变更时实现注册表失效机制。

陷阱 2:Docker 容器配置路径隔离

问题:在 Docker 中运行时,容器内的 ~/.openclaw/ 路径与主机路径不同。如果卷未正确映射,文件后端 SecretRef 可能无法解析。

症状

$ docker run openclaw/openclaw status
Error: channels.feishu.appSecret: unresolved SecretRef "file:localfile:/channels/feishu/appSecret"

缓解措施:挂载 secrets 目录:

docker run -v ~/.openclaw/secrets.json:/root/.openclaw/secrets.json openclaw/openclaw status

陷阱 3:Windows 路径分隔符不匹配

问题:在 Windows 上,文件路径使用反斜杠,但 SecretRef 路径在内部被规范化为正斜杠。

症状

Error: channels.feishu.appSecret: unresolved SecretRef "file:localfile:\channels\feishu\appSecret"

缓解措施:即使在 Windows 上也使用正斜杠作为 SecretRef ID,或在配置文件中使用 path.normalize() 包装器。

陷阱 4:Secrets 文件权限被拒绝

问题:secrets 文件存在但权限不正确,阻止 localfile provider 读取。

症状

Error: channels.feishu.appSecret: file read error: EACCES: permission denied

缓解措施

# Linux/macOS
chmod 600 ~/.openclaw/secrets.json

# Verify
ls -la ~/.openclaw/secrets.json
# -rw-------  1 user  staff  128 Mar 23 14:30 ~/.openclaw/secrets.json

陷阱 5:过期的网关运行时快照

问题:网关运行时在创建或更新 secrets 文件之前就已启动。缓存的快照包含未解析的 SecretRef。

症状:从网关 API 调用命令成功,但从 CLI 调用失败。

缓解措施

# Restart the gateway to refresh its config snapshot
openclaw gateway stop
openclaw gateway start

# Verify snapshot is fresh
openclaw gateway config-snapshot --resolved | jq '.channels.feishu'

陷阱 6:在测试中混合使用原始配置和已解析配置

问题:单元测试可能将原始配置对象直接传递给期望已解析配置的函数。

症状:测试在本地通过但在 CI 中失败并出现 SecretRef 错误。

缓解措施:始终在测试中解析配置:

// Before (flaky)
const result = await processStatus(rawConfig);

// After (correct)
const resolvedConfig = await resolveCommandSecretRefsViaGateway(rawConfig, mockGateway);
const result = await processStatus(resolvedConfig);

🔗 相关错误

错误代码参考表

错误代码错误消息模式根因修复优先级
SECRETF001unresolved SecretRef访问前配置未解析
SECRETF002file read error: EACCESsecrets 文件权限问题
SECRETF003file read error: ENOENTsecrets 文件不存在
PLUGIN001plugin load failed插件注册在初始化期间抛出
PLUGIN002plugin registry already loaded注册表使用错误配置缓存
CONFIG001invalid config schema配置文件 JSON 格式错误
CONFIG002missing required field必需的配置字段缺失

历史相关问题

  • Issue #447: `openclaw health` 当 Feishu channel secret 使用环境变量 SecretRef 时失败
    症状重叠:两个问题都涉及插件加载期间的 SecretRef 解析失败。
    区别:Issue #447 针对使用 env 后端 secrets 的 `health` 命令;此问题针对使用文件后端 secrets 的 `status`。
    共同根因:在命令级配置解析之前过早加载插件。
  • Issue #389: `ensurePluginRegistryLoaded()` 忽略传递的配置参数
    症状重叠:函数接受配置覆盖但不使用它。
    区别:Issue #389 是 API 设计问题;此问题是用户可见的故障后果。
    共同修复:对 `options?.config ?? loadConfig()` 的相同单行更改。
  • Issue #412: `loadPlugins` 回调在命令处理器上下文可用之前求值
    症状重叠:路由级插件预加载无法访问已解析配置。
    区别:Issue #412 是架构分析;此问题是通过 SecretRef 的具体复现。
    共同修复:禁用需要已解析配置的 命令的路由级插件预加载。
  • Issue #523: Windows 上文件后端 SecretRef provider 使用反斜杠路径时抛出异常
    症状重叠:`status` 命令中的文件后端 SecretRef 使用。
    区别:Issue #523 是路径分隔符规范化问题;此问题是生命周期排序问题。
    潜在交互:Windows 用户可能同时遇到两个问题。

相关配置模式

  • 环境变量 SecretRef: `{"source": "env", "provider": "process", "id": "FEISHU_APP_SECRET"}`
  • Vault SecretRef: `{"source": "vault", "provider": "hashicorp", "id": "secret/channels#feishu-app-secret"}`
  • AWS Secrets Manager: `{"source": "aws", "provider": "secretsmanager", "id": "prod/feishu/app-secret"}`

调试命令

# Enable verbose plugin loading
OPENCLAW_DEBUG=plugin-load openclaw status

# Enable config resolution tracing
OPENCLAW_DEBUG=config-resolution openclaw status

# Check loaded plugins
openclaw plugin list

# Verify SecretRef resolution chain
openclaw secret-refs --trace channels.feishu.appSecret

# Dump raw vs resolved config
openclaw config --raw | jq '.channels.feishu.appSecret'
openclaw config --resolved | jq '.channels.feishu.appSecret'

依据与来源

本故障排除指南由 FixClaw 智能管线从社区讨论中自动合成。