Kanal-Konfiguration
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
channel-config.ts:60-165—resolveChannelEntryMatch/resolveChannelEntryMatchWithFallback, matched direct / parent / wildcard.zod-schema.channels-config.ts:52-69—ChannelsSchema-Spitze, passthrough toleriert Plugin-Felder.zod-schema.core.ts:33-90—SecretRefSchema/SecretInputSchema, drei Secret-Quellen.zod-schema.providers-core.ts:249-409—TelegramAccountSchemaBase/TelegramConfigSchema, Beispiel für Multi-Account-Schema.SlackConfigSchema Position:1054— Slack Multi-Account-Schema.WhatsAppConfigSchema:238— WhatsApp nutztz.preprocessfür Altkonfig-Kompat.types.channels.ts:127-146—ChannelsConfig-Schnittstelle, typed Felder + Open-World-Index-Signatur.plugins/config-schema.ts:27-49—AllowFromEntrySchema/buildCatchallMultiAccountChannelSchemagemeinsamer Builder.config-presence.ts:47-66—hasMeaningfulChannelConfig/listExplicitlyDisabledChannelIdsForConfig.
Datenfluss
Der channels-Block auf Spitze von openclaw.json geht durch ChannelsSchema (zod-schema.channels-config.ts:52):
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 passthrough — channels.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):
/** 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):
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):
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:
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ässtchannels.nostr/channels.matrixals Plugin-Felder auf der Spitze nicht fehlschlagen, validiert sie aber auch nicht. Validierung sinkt in das vom Kanal-Plugin mitgebrachteChannelConfigSchema; Kanäle ohne registriertes Schema sind unvalidated. - Legacy ACP bindings meldet Fehler:
addLegacyChannelAcpBindingIssues(zod-schema.channels-config.ts:20) scannt rekursiv nachbindings.acpund bei Fundctx.addIssuemit dem Hinweis „Legacy channel-local ACP bindings were removed; use top-levelbindings[]entries". Alte Konfigurationen bleiben hier hängen. - Secret-Ref-Validierung streng: env-Typ
idmuss/^[A-Z][A-Z0-9_]{0,127}$/(zod-schema.core.ts:44), file-Typidmuss 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 beidmPolicy: "allowlist", dassallowFromnicht 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 alsz.record()— kann „bekannte Felder" vs „dynamische Account-Schlüssel" unterscheiden; wenn später reserved Felder wiedefaultAccountdazukommen, kollidieren sie nicht mit Account-IDs. - enabled=false nicht configured:
hasMeaningfulChannelConfig(config-presence.ts:47) schließtenabledexplizit 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.