Skip to content

Kanal-Konfiguration

源码版本v2026.6.11

Verantwortung

Die Kanal-Konfiguration (channel config) ist die Parse- und Match-Schicht für den Block channels.* in openclaw.json. Sie macht drei Dinge: vom Nutzer geschriebenes JSON in strukturierte Typen parsen, nach Kanal-ID eine Account-Konfiguration (account config) matchen, und sensible Felder wie token / botToken / webhookSecret von hardcoded Strings zu Secret-Referenzen (env / file / exec) erheben.

Sie entscheidet nicht „welcher Kanal soll starten" — das machen Kanal-Registry und config-presence.ts. Sie ist nur zuständig für „was der Nutzer geschrieben hat, ob es korrekt ist, über welchen Schlüssel auf welche Account-Konfiguration gematcht wird".

Designmotivation

Vier praktische Gründe.

Erstens: 22 Kanäle mit unterschiedlichen Feldern. Telegram hat botToken + webhookUrl + pollingStallThresholdMs, Slack hat botToken + appToken + socketMode, WhatsApp hat authDir, Discord hat guilds + channels. In einem ChannelsConfig-Typ würde das zu tausenden Zeilen Union-Typen aufblähen; jede Kanaländerung berührte die Kerntypen.

Zweitens: Multi-Account. Telegram kann mehrere Bots tragen, Slack mehrere Workspaces, jeder Account unabhängig konfiguriert. Das braucht das vereinheitlichte Muster accounts: Record<string, AccountConfig> + defaultAccount: string, mit kanalspezifischen Feldern hinab auf Account-Ebene.

Drittens: Secrets nicht hardcoded. botToken: "123456:ABC..." in openclaw.json — leakt die Datei, leakt der Token. SecretInputSchema = z.union([z.string(), SecretRefSchema]) lässt den Nutzer sowohl alte Inline-Strings schreiben als auch strukturierte Referenzen wie {source: "env", provider: "default", id: "TELEGRAM_BOT_TOKEN"}.

Viertens: Konfigurations-Matching muss direct / parent / wildcard unterstützen. Derselbe Kanal kann mehrere Konfigurationen haben: eine für konkrete Account-ID, eine für den Elternkanal, eine Wildcard. Beim Parsen wird nach Priorität zurückgefallen und matchSource markiert, damit Downstream weiß, welche Ebene getroffen wurde.

Schlüsseldateien

Datenfluss

Der channels-Block auf Spitze von openclaw.json geht durch 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>;

Auf Spitze gibt es nur defaults und modelByChannel als strict-Felder, der Rest geht durch passthroughchannels.telegram / channels.slack / channels.nostr / channels.zalo werden hier nicht feldvalidiert, die Feldvalidierung sinkt in den jeweiligen Kanal-Schema TelegramConfigSchema / SlackConfigSchema / WhatsAppConfigSchema. .superRefine scannt nur das legacy bindings.acp-Feld (entfernt); findet es welches, wird gewarnt, dass auf das top-level bindings[] migriert werden soll.

Warum passthrough? Weil Plugin-Kanäle eine offene Welt (open-world) sind — types.channels.ts (types.channels.ts:127) schreibt 10 Kernkanäle (discord / googlechat / imessage / irc / msteams / signal / slack / telegram / whatsapp u. a.) als typed Felder, der Rest geht über [key: string]: OpenWorldChannelConfig, wobei OpenWorldChannelConfig einfach ReturnType<typeof JSON.parse> ist — komplett offen.

Secret-Referenz ist die zentrale Abstraktion der Konfigurationsschicht (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 nutzt discriminatedUnion("source", ...) mit drei Quellen: env liest aus Umgebungsvariable, id matcht /^[A-Z][A-Z0-9_]{0,127}$/ wie TELEGRAM_BOT_TOKEN; file liest aus Datei, id ist ein JSON-Pointer wie /providers/openai/apiKey oder value im Einwertmodus; exec ruft ein externes Kommando, um das Secret zu holen.

SecretInputSchema ist z.union([z.string(), SecretRefSchema]) — alte Inline-Strings bleiben kompatibel, aber .register(sensitive) markiert das Feld als sensibel (zod-schema.providers-core.ts:269):

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

So maskiert openclaw doctor / Log-Output den Literalwert von botToken.

Jedes Kanal-Schema kombiniert allgemeine und kanalspezifische Felder; Telegram ist das typische Multi-Account-Beispiel (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 ist die Feldmenge auf Kanalebene, TelegramConfigSchema fügt accounts-Multi-Account-Container und defaultAccount hinzu — buildCatchallMultiAccountChannelSchema (plugins/config-schema.ts:42) ist der generische Helper für dieses Muster:

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) lässt jeden Schlüssel unter accounts nach accountSchema validieren — ein neuer Account erfordert keine Schemaänderung.

Beim Matchen läuft resolveChannelEntryMatchWithFallback (channel-config.ts:83) mit Prioritäts-Fallback — direct-Treffer liefert direkt { matchKey, matchSource: "direct" }; ohne Treffer geht es zu normalized direct (mit normalizeKey normalisiert), dann parent, normalized parent, schließlich wildcard. Jeder Schritt schreibt matchKey und matchSource ins Ergebnis; Downstream nutzt applyChannelMatchMeta, um die zwei Felder auf das finale Config-Objekt zu kopieren — fürs Auditieren „über welchen Schlüssel wurde diese Konfiguration getroffen".

normalizeKey ist eine optionale Normalisierungsfunktion, z. B. normalizeChannelSlug (channel-config.ts:47) das #general, General, GENERAL ROOM alle zu general normalisiert — Nutzer müssen in der Konfiguration keine exakte Kanalnamen-Schreibung treffen.

hasMeaningfulChannelConfig unterscheidet „Inhalt konfiguriert" von „nur enabled=false" (config-presence.ts:47): es prüft Object.keys(value).some((key) => key !== "enabled")enabled allein gilt nicht als „Konfiguration vorhanden", sondern nur als Betriebswillen; so meldet openclaw status einen explizit deaktivierten Kanal nicht fälschlich als „konfiguriert, aber nicht gestartet".

Der gesamte Parse- und Match-Fluss:

Grenzen und Fehler

  • passthrough toleriert Plugin-Felder: ChannelsSchema.passthrough() (zod-schema.channels-config.ts:65) lässt channels.nostr / channels.matrix als Plugin-Felder auf der Spitze nicht fehlschlagen, validiert sie aber auch nicht. Validierung sinkt in das vom Kanal-Plugin mitgebrachte ChannelConfigSchema; Kanäle ohne registriertes Schema sind unvalidated.
  • Legacy ACP bindings meldet Fehler: addLegacyChannelAcpBindingIssues (zod-schema.channels-config.ts:20) scannt rekursiv nach bindings.acp und bei Fund ctx.addIssue mit dem Hinweis „Legacy channel-local ACP bindings were removed; use top-level bindings[] entries". Alte Konfigurationen bleiben hier hängen.
  • Secret-Ref-Validierung streng: env-Typ id muss /^[A-Z][A-Z0-9_]{0,127}$/ (zod-schema.core.ts:44), file-Typ id muss absoluter JSON-Pointer sein. Falsches Format schlägt fehl ohne stillen Fallback auf Literalwert.
  • Allowlist muss allowFrom haben: requireOpenAllowFrom (zod-schema.providers-core.ts:409) erzwingt bei dmPolicy: "allowlist", dass allowFrom nicht leer ist, sonst scheitert safeParse. Verhindert „dachte, Whitelist an, tatsächlich darf jeder DM".
  • accounts als catchall nicht record: z.object({}).catchall(accountSchema) (plugins/config-schema.ts:46) ist eine Schicht mehr als z.record() — kann „bekannte Felder" vs „dynamische Account-Schlüssel" unterscheiden; wenn später reserved Felder wie defaultAccount dazukommen, kollidieren sie nicht mit Account-IDs.
  • enabled=false nicht configured: hasMeaningfulChannelConfig (config-presence.ts:47) schließt enabled explizit aus; Setup-Fluss missversteht „explizit deaktiviert" nicht als „konfiguriert aber nicht gestartet".
  • WhatsApp über preprocess: WhatsAppConfigSchema = z.preprocess(...) (zod-schema.providers-whatsapp.ts:238) migriert vor dem Parsen die Altkonfig — nicht superRefine; mutate-before-validate ist für Historien-Kompatibilität wichtig, aber beim Debuggen ist zu beachten, dass safeParse bereits die migrierten Daten sieht.

Zusammenfassung

Die Kanal-Konfigurationsschicht saugt die Feldunterschiede von 22 Kanälen in ChannelsSchema.passthrough() + jeweilsem Kanal-ConfigSchema auf zwei Ebenen auf — die Spitze validiert nur Common-Felder und Legacy; Feldvalidierung sinkt hinab. Multi-Account-Muster vereinheitlicht über buildCatchallMultiAccountChannelSchema; Secrets nutzen SecretInputSchema mit drei Quellen (env / file / exec) und Inline-Kompat; Matching läuft über fünf Stufen (direct / normalized / parent / normalized parent / wildcard) und schreibt matchSource fürs Downstream-Audit.

Wie aus der Kanal-Registry der Plugin geholt und über sein config.listAccountIds / config.resolveAccount diese Konfigurationen in strukturierte Account-Objekte gewandelt werden, siehe Kanal-Adapter. Gesamter Lade- und Validierungsfluss von openclaw.json siehe openclaw.json-Konfiguration; Organisation aller zod-Schemas siehe zod-Schema-System.