Skip to content

通道設定

源码版本v2026.6.11

職責

通道設定 (channel config) 是 openclaw.jsonchannels.* 這一坨欄位的解析與匹配層。它管三件事:把使用者寫的 JSON 解析成結構化型別、按通道 id 匹配出帳號設定 (account config)、把 token / botToken / webhookSecret 這類敏感欄位從硬編碼字串升級成 secret reference (env / file / exec)。

它不做「哪個通道該啟動」的決策——那是 通道登錄表 配合 config-presence.ts 幹的。它只負責「使用者寫了什麼、寫得對不對、按什麼鍵匹配到哪條帳號設定」。

設計動機

四個現實原因。

第一,22 個通道欄位各不相同。Telegram 有 botToken + webhookUrl + pollingStallThresholdMs,Slack 有 botToken + appToken + socketMode,WhatsApp 有 authDir,Discord 有 guilds + channels。塞進一個 ChannelsConfig 型別會變成幾千行聯合型別,改一個通道要動核心型別。

第二,多帳號。Telegram 可掛多 bot,Slack 可掛多 workspace,每個帳號獨立設定。需要 accounts: Record<string, AccountConfig> + defaultAccount: string 的統一模式,各通道欄位下推到 account 層。

第三,secret 不能硬編碼botToken: "123456:ABC..." 寫在 openclaw.json 裡一旦檔案洩露 token 就洩了。SecretInputSchema = z.union([z.string(), SecretRefSchema]) 讓使用者既能寫老式 inline 字串,也能寫 {source: "env", provider: "default", id: "TELEGRAM_BOT_TOKEN"} 這種結構化引用。

第四,設定匹配要支援 direct / parent / wildcard。同一個通道可能有多條設定:具體帳號 id 一份、父級通道一份、萬用字元一份。解析時按優先級回退,並標記 matchSource 讓下游知道命中了哪一級。

關鍵檔案

資料流

openclaw.json 頂層 channels 欄位走 ChannelsSchema(zod-schema.channels-config.ts:52):

typescript
export const ChannelsSchema: z.ZodType<ChannelsConfig | undefined> = z
  .object({
    defaults: z
      .object({
        groupPolicy: GroupPolicySchema.optional(),
        contextVisibility: ContextVisibilityModeSchema.optional(),
        heartbeat: ChannelHeartbeatVisibilitySchema,
        botLoopProtection: ChannelBotLoopProtectionSchema.optional(),
      })
      .strict()
      .optional(),
    modelByChannel: ChannelModelByChannelSchema,
  })
  .passthrough() // Allow extension channel configs (nostr, matrix, zalo, etc.)
  .superRefine((value, ctx) => {
    addLegacyChannelAcpBindingIssues(value, ctx);
  })
  .optional() as z.ZodType<ChannelsConfig | undefined>;

頂層只有 defaultsmodelByChannel 兩個 strict 欄位,其餘全走 passthrough——channels.telegram / channels.slack / channels.nostr / channels.zalo 這些鍵在頂層 schema 這裡不做欄位級校驗,欄位校驗下放到各通道自己的 TelegramConfigSchema / SlackConfigSchema / WhatsAppConfigSchema.superRefine 只掃描遺留的 bindings.acp 欄位(已廢棄),有就報錯讓使用者遷移到頂層 bindings[]

為什麼用 passthrough?因為外掛通道是開放世界 (open-world)——types.channels.ts(types.channels.ts:127)的 ChannelsConfig 介面把 10 個核心通道 (discord / googlechat / imessage / irc / msteams / signal / slack / telegram / whatsapp 等) 寫成 typed 欄位,其餘全走 [key: string]: OpenWorldChannelConfig,OpenWorldChannelConfig 就是 ReturnType<typeof JSON.parse>,完全開放。

Secret 引用是設定層的核心抽象(zod-schema.core.ts:83):

typescript
/** Config-level secret reference schema shared by model/provider/plugin credential fields. */
export const SecretRefSchema = z.discriminatedUnion("source", [
  EnvSecretRefSchema,
  FileSecretRefSchema,
  ExecSecretRefSchema,
]);

/** Accepts either legacy inline secret strings or structured secret references. */
export const SecretInputSchema = z.union([z.string(), SecretRefSchema]);

SecretRefSchemadiscriminatedUnion("source", ...),三種 source:env 從環境變數讀,id 匹配 /^[A-Z][A-Z0-9_]{0,127}$/TELEGRAM_BOT_TOKEN;file 從檔案讀,id 是 JSON pointer 如 /providers/openai/apiKey 或單值模式的 value;exec 呼叫外部指令拿 secret。

SecretInputSchemaz.union([z.string(), SecretRefSchema])——老式 inline 字串仍相容,但 .register(sensitive) 把欄位標記為敏感(zod-schema.providers-core.ts:269):

typescript
botToken: SecretInputSchema.optional().register(sensitive),

這樣 openclaw doctor / 日誌輸出時會把 botToken 字面值脫敏。

每個通道自己的 schema 把通用欄位 + 通道專屬欄位組合起來,Telegram 是最典型的多帳號例子(zod-schema.providers-core.ts:405):

typescript
export const TelegramConfigSchema = TelegramAccountSchemaBase.extend({
  accounts: z.record(z.string(), TelegramAccountSchema.optional()).optional(),
  defaultAccount: z.string().optional(),
}).superRefine((value, ctx) => {
  requireOpenAllowFrom({
    // ...
  });
});

TelegramAccountSchemaBase 是通道級欄位集合,TelegramConfigSchema 在此基礎上加 accounts 多帳號容器和 defaultAccount——buildCatchallMultiAccountChannelSchema (plugins/config-schema.ts:42) 是這種模式的通用 helper:

typescript
export function buildCatchallMultiAccountChannelSchema<T extends ExtendableZodObject>(
  accountSchema: T,
): T {
  return accountSchema.extend({
    accounts: z.object({}).catchall(accountSchema).optional(),
    defaultAccount: z.string().optional(),
  }) as T;
}

catchall(accountSchema)accounts 下任意 key 都按 accountSchema 校驗——這樣加新帳號不需要改 schema。

匹配設定時走 resolveChannelEntryMatchWithFallback(channel-config.ts:83),核心是按優先級回退——direct 命中直接返回 { matchKey, matchSource: "direct" };沒命中就走 normalized direct (用 normalizeKey 歸一 key 再匹配),再走 parent、normalized parent,最後落 wildcard。每一步都把 matchKeymatchSource 寫進結果,下游用 applyChannelMatchMeta 把這兩個欄位複製到最終 config 物件上,方便審計「這條設定是從哪個 key 命中的」。

normalizeKey 是可選的歸一函式,比如 normalizeChannelSlug(channel-config.ts:47)把 #generalGeneralGENERAL ROOM 都歸一成 general——使用者在設定裡寫 channel 名時不強求精確匹配。

hasMeaningfulChannelConfig 區分「設定了內容」和「只是 enabled=false」(config-presence.ts:47):它檢查 Object.keys(value).some((key) => key !== "enabled")——enabled 單獨不算「有設定」,只是營運意圖,這樣 openclaw status 不會把「顯式禁用」的通道誤報為「已設定未啟用」。

整個設定解析與匹配流程:

邊界與失敗

  • passthrough 容忍外掛欄位:ChannelsSchema.passthrough()(zod-schema.channels-config.ts:65)讓 channels.nostr / channels.matrix 這種外掛欄位不在頂層報錯,但也不校驗。校驗下放到各通道外掛自帶的 ChannelConfigSchema,沒註冊 schema 的通道就是 unvalidated。
  • legacy ACP bindings 報錯:addLegacyChannelAcpBindingIssues(zod-schema.channels-config.ts:20)遞迴掃描 bindings.acp,發現就 ctx.addIssue,提示「Legacy channel-local ACP bindings were removed; use top-level bindings[] entries」。舊設定升級會被這裡擋住。
  • secret ref 校驗嚴格:env 類 id 必須 /^[A-Z][A-Z0-9_]{0,127}$/(zod-schema.core.ts:44),file 類 id 必須是絕對 JSON pointer。格式不對直接報錯,不會靜默退回字面值。
  • allowlist 必須有 allowFrom:requireOpenAllowFrom(zod-schema.providers-core.ts:409)在 dmPolicy: "allowlist" 時強制要求 allowFrom 非空,否則 safeParse 失敗。防止「以為開了白名單其實誰都能 DM」。
  • accounts 用 catchall 不是 record:z.object({}).catchall(accountSchema)(plugins/config-schema.ts:46)比 z.record() 多一層——能區分「已知欄位」和「動態帳號 key」,未來加 defaultAccount 這種保留欄位時不會跟帳號 id 衝突。
  • enabled=false 不是 configured:hasMeaningfulChannelConfig(config-presence.ts:47)顯式排除 enabled,setup 流程不會把「顯式禁用」誤認為「已設定但未啟動」。
  • WhatsApp 走 preprocess:WhatsAppConfigSchema = z.preprocess(...)(zod-schema.providers-whatsapp.ts:238)在解析前先遷移老設定,不是 superRefine——mutate-before-validate 對歷史相容很關鍵,但除錯時要注意 safeParse 收到的 data 已經是遷移後的。

小結

通道設定層把 22 個通道各自的欄位差異吸收在 ChannelsSchema.passthrough() + 各通道 ConfigSchema 兩級——頂層只管公共欄位和 legacy 偵測,欄位級校驗下放。多帳號模式用 buildCatchallMultiAccountChannelSchema 統一,secret 用 SecretInputSchema 三源 (env / file / exec) 相容字面值,匹配走 direct / normalized / parent / wildcard 五級回退並寫 matchSource 讓下游審計。

怎麼從 通道登錄表 拿到 plugin 後呼叫它的 config.listAccountIds / config.resolveAccount 把這些設定轉成結構化 account 物件,看 通道介面卡openclaw.json 的整體載入與校驗流程看 openclaw.json 設定,所有 zod schema 的組織方式看 zod schema 體系