Skip to content

Configuration des canaux

源码版本v2026.6.11

Responsabilités

La configuration de canal (channel config) est la couche de parsing et de matching pour les champs channels.* dans openclaw.json. Elle gère trois choses: parser le JSON saisi par l'utilisateur en un type structuré, matcher la configuration de compte (account config) par id de canal, faire évoluer les champs sensibles comme token / botToken / webhookSecret depuis une chaîne codée en dur vers une secret reference (env / file / exec).

Elle ne décide pas « quel canal doit démarrer » — c'est l'affaire du registre des canaux combiné à config-presence.ts. Elle ne fait que traiter « ce que l'utilisateur a écrit, si c'est correct, et par quelle clé on matche quelle entrée de compte ».

Motivation de conception

Quatre raisons concrètes.

Premièrement, les champs des 22 canaux diffèrent. Telegram a botToken + webhookUrl + pollingStallThresholdMs; Slack a botToken + appToken + socketMode; WhatsApp a authDir; Discord a guilds + channels. Tout mettre dans un type ChannelsConfig donnerait un type union de plusieurs milliers de lignes, et modifier un canal toucherait au type cœur.

Deuxièmement, le multi-comptes. Telegram peut avoir plusieurs bots, Slack plusieurs workspaces, chaque compte ayant sa config indépendante. Il faut un patron unifié accounts: Record<string, AccountConfig> + defaultAccount: string, avec les champs de chaque canal poussés au niveau account.

Troisièmement, les secrets ne doivent pas être codés en dur. botToken: "123456:ABC..." dans openclaw.json, dès que le fichier fuit, le token fuit. SecretInputSchema = z.union([z.string(), SecretRefSchema]) permet d'écrire soit une ancienne chaîne inline, soit une référence structurée {source: "env", provider: "default", id: "TELEGRAM_BOT_TOKEN"}.

Quatrièmement, le matching doit supporter direct / parent / wildcard. Un même canal peut avoir plusieurs configurations: une pour un id de compte spécifique, une pour le canal parent, une wildcard. Le parsing repli par ordre de priorité et marque matchSource pour que le sache quel niveau a été touché.

Fichiers clés

Flux de données

Le champ channels au top level de openclaw.json passe par 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>;

Le top-level n'a que deux champs stricts defaults et modelByChannel, tout le reste passe par passthroughchannels.telegram / channels.slack / channels.nostr / channels.zalo ne sont pas validés au niveau des champs par le schema top-level. La validation des champs est déléguée à chaque TelegramConfigSchema / SlackConfigSchema / WhatsAppConfigSchema propre au canal. Le .superRefine ne scanne que les champs bindings.acp legacy (deprecated), et émet un issue pour forcer la migration vers le bindings[] top-level.

Pourquoi passthrough? Parce que les canaux de plugin sont open-world — types.channels.ts (types.channels.ts:127) typifie 10 canaux cœur (discord / googlechat / imessage / irc / msteams / signal / slack / telegram / whatsapp etc.) comme champs typés, tout le reste passe par [key: string]: OpenWorldChannelConfig, où OpenWorldChannelConfig est ReturnType<typeof JSON.parse>, totalement ouvert.

La référence aux secrets est l'abstraction centrale de la couche de config (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]);

SecretRefSchema utilise discriminatedUnion("source", ...) avec trois sources: env lit depuis une variable d'environnement, id matchant /^[A-Z][A-Z0-9_]{0,127}$/ comme TELEGRAM_BOT_TOKEN; file lit depuis un fichier, id est un JSON pointer comme /providers/openai/apiKey ou value en mode simple; exec appelle une commande externe pour obtenir le secret.

SecretInputSchema est z.union([z.string(), SecretRefSchema]) — l'ancienne chaîne inline reste compatible, mais .register(sensitive) marque le champ comme sensible (zod-schema.providers-core.ts:269):

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

Ainsi openclaw doctor et les logs masquent la valeur littérale de botToken.

Le schema propre à chaque canal combine champs communs + champs spécifiques; Telegram est l'exemple canonique multi-comptes (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 est le set de champs au niveau canal; TelegramConfigSchema y ajoute le conteneur multi-comptes accounts et defaultAccount. buildCatchallMultiAccountChannelSchema (plugins/config-schema.ts:42) est le helper générique de ce patron:

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) fait que toute clé sous accounts est validée selon accountSchema — ainsi ajouter un nouveau compte ne demande pas de toucher au schema.

Le matching passe par resolveChannelEntryMatchWithFallback (channel-config.ts:83); le cœur en est le repli par priorité — un hit direct renvoie directement { matchKey, matchSource: "direct" }; en cas d'échec on passe au normalized direct (match après normalisation de la clé via normalizeKey), puis parent, puis normalized parent, et enfin wildcard. Chaque étape écrit matchKey et matchSource dans le résultat; le aval utilise applyChannelMatchMeta pour copier ces deux champs sur l'objet config final, pour audit « quelle clé a matché cette config ».

normalizeKey est une fonction de normalisation optionnelle; par exemple normalizeChannelSlug (channel-config.ts:47) ramène #general, General, GENERAL ROOM à general — l'utilisateur n'a pas besoin de matching exact quand il écrit le nom du canal dans la config.

hasMeaningfulChannelConfig distingue « a configuré du contenu » de « juste enabled=false » (config-presence.ts:47): il vérifie Object.keys(value).some((key) => key !== "enabled")enabled seul ne compte pas comme « a de la config », juste une intention opérationnelle; ainsi openclaw status ne prend pas un canal « explicitement désactivé » pour « configuré mais non actif ».

Tout le flux de parsing et matching:

Limites et modes d'échec

  • passthrough tolère les champs de plugin: ChannelsSchema.passthrough() (zod-schema.channels-config.ts:65) fait que channels.nostr / channels.matrix ne lancent pas d'erreur au top-level, mais ne sont pas non plus validés. La validation est déléguée au ChannelConfigSchema embarqué par chaque plugin de canal; sans schema enregistré, le canal est unvalidated.
  • legacy ACP bindings en erreur: addLegacyChannelAcpBindingIssues (zod-schema.channels-config.ts:20) scanne récursivement bindings.acp, émet un ctx.addIssue pour dire « Legacy channel-local ACP bindings were removed; use top-level bindings[] entries ». Une vieille config sera bloquée ici à la mise à jour.
  • Validation stricte des secret refs: pour la source env, l'id doit matcher /^[A-Z][A-Z0-9_]{0,127}$/ (zod-schema.core.ts:44); pour file, l'id doit être un absolute JSON pointer. Sinon erreur, pas de repli silencieux vers la valeur littérale.
  • allowlist doit avoir allowFrom: requireOpenAllowFrom (zod-schema.providers-core.ts:409) force allowFrom non vide quand dmPolicy: "allowlist", sinon safeParse échoue. Empêche « on croit la allowlist activée alors que tout le monde peut DM ».
  • accounts en catchall pas record: z.object({}).catchall(accountSchema) (plugins/config-schema.ts:46) est plus riche que z.record() — permet de distinguer champs connus et clés de compte dynamiques, et d'ajouter plus tard des champs réservés comme defaultAccount sans collision avec les ids de compte.
  • enabled=false n'est pas configured: hasMeaningfulChannelConfig (config-presence.ts:47) exclut explicitement enabled; le flow setup ne prendra pas un « explicitement désactivé » pour un « configuré mais non démarré ».
  • WhatsApp passe par preprocess: WhatsAppConfigSchema = z.preprocess(...) (zod-schema.providers-whatsapp.ts:238) migre la vieille config avant parsing, pas superRefine — mutate-before-validate est crucial pour la compat historique, mais en debug il faut savoir que le data reçu par safeParse est déjà migré.

Résumé

La couche de config de canal absorbe les différences de champs entre les 22 canaux via ChannelsSchema.passthrough() + un ConfigSchema par canal — le top-level ne gère que les champs communs et la détection legacy, la validation des champs est déléguée. Le patron multi-comptes est unifié par buildCatchallMultiAccountChannelSchema; les secrets via SecretInputSchema à trois sources (env / file / exec) compatibles avec les valeurs littérales; le matching suit un repli cinq niveaux direct / normalized / parent / wildcard et écrit matchSource pour audit aval.

Comment on obtient l'entrée plugin depuis le registre des canaux pour appeler son config.listAccountIds / config.resolveAccount qui transforme ces configs en objets account structurés, voir adaptateur de canal. Le flux général de chargement et validation de openclaw.json est dans configuration openclaw.json; l'organisation des zod schemas dans système zod schema.