Skip to content

チャネル登録表

源码版本v2026.6.11

責務

チャネル登録表 (channel registry) は OpenClaw が「22 個のチャットチャネル + 任意のサードパーティプラグインチャネル」を統一してクエリ可能な辞書にする層です。外部には 4 件事だけを公開します:登録済みチャネル id の列挙,ユーザ入力の別名 (alias) を正規化 (normalize) して正規 id に,id または alias でチャネルプラグイン (channel plugin) エントリを検索,setup / status 画面向けに短い紹介文を組み立て。

登録表自体はチャネルのランタイム実装を読み込みません——メタデータ (metadata) と検索インデックスだけを持ちます。実際に startAccountsendMessage などのアクションを呼ぶのは チャネルアダプタ の仕事で,登録表は「telegram という id がどの plugin エントリに対応し,どんな別名を持つか」を教えるだけです。

設計動機

なぜ import { telegramPlugin } from "..." と直接せず,登録表を単独で設けるのか?3 つの現実的理由:

第一に,チャネル数は固定ですが別名があります。telegram はユーザ設定内で tgTelegramTELEGRAM と書かれる可能性があり,登録表はこれらをすべて同じ正規 id に統一しなければなりません,さもなくば下流の id 索引 map すべてが大小文字と別名処理を繰り返します。

第二に,チャネルには bundled と loaded の 2 層があります。bundled は生成コードに組み込みのメタデータ,loaded はランタイムにプラグイン npm パッケージから読み込まれたもの。loaded は bundled より優先され,telegram プラグインパッケージをインストールするとビルトイン版を pin または override できますが,未インストール時は bundled で兜底できます。登録表はこの 2 層を同じクエリビューにマージしなければなりません。

第三に,プラグインhot 登録:起動スナップショット後に登録されたチャネルも検索可能でなければなりません(#94127 系の問題を修正)。登録表は起動時に一度 build するだけではいけませんが,毎回のクエリで再計算してもいけません——だからスナップショットバージョン番号 (version) をキャッシュ無効化に使います。

主要ファイル

データフロー

22 個の bundled チャネル id は generated metadata から来ます。ids.ts は起動時にこれをフィルタしてソート(ids.ts:21):

typescript
function listBundledChatChannelEntries(): BundledChatChannelEntry[] {
  return GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA.filter((entry) => entry.configurable !== false)
    .map((entry) => ({
      id: normalizeOptionalLowercaseString(entry.channelId) ?? entry.channelId,
      aliases: entry.aliases ?? [],
      order: entry.order ?? Number.MAX_SAFE_INTEGER,
    }))
    .toSorted(
      (left, right) =>
        left.order - right.order || left.id.localeCompare(right.id, "en", { sensitivity: "base" }),
    );
}

実際の 22 個の id は:clickclack / discord / feishu / googlechat / imessage / irc / line / matrix / mattermost / msteams / nostr / qqbot / raft / signal / slack / sms / telegram / tlon / twitch / whatsapp / zalo / zalouser。

正規化 (normalization) は「alias 表 → runtime catalog」の 2 段階フォールバック(ids.ts:84)で走ります:

typescript
export function normalizeChatChannelId(raw?: string | null): ChatChannelId | null {
  const normalized = normalizeOptionalLowercaseString(raw);
  if (!normalized) {
    return null;
  }
  const resolved = CHAT_CHANNEL_ALIASES[normalized] ?? normalized;
  return CHAT_CHANNEL_ID_SET.has(resolved)
    ? resolved
    : normalizeRuntimeBundledChatChannelId(normalized);
}

ここではまずコンパイル時生成の CHAT_CHANNEL_ALIASES でよくある別名をヒットさせ,ヒットしなければ listRuntimeBundledChatChannelEntries で兜底します——後者は bundled-channel-catalog-read.ts を読み,コンパイル時生成されていないがランタイムに動的登録された bundled metadata をカバーします。

ランタイム検索は registry-lookup.ts がスナップショット + バージョン番号キャッシュ(registry-lookup.ts:42)で行います:

typescript
function buildRegisteredChannelPluginLookup(): RegisteredChannelPluginLookup {
  const { registry, version } = getActivePluginChannelRegistrySnapshotFromState();
  const channels = Array.isArray(registry?.channels) ? registry.channels : undefined;
  const channelCount = channels?.length ?? 0;
  const cached = registeredChannelPluginLookup;
  if (
    cached &&
    cached.registry === registry &&
    cached.channels === channels &&
    cached.channelCount === channelCount &&
    cached.version === version
  ) {
    return cached;
  }
  // ... byKey / byId インデックスを再構築
}

再構築条件が4 つの参照相等に version を 1 つ加えていることに注意。version は plugin registry が進める整数で,新プラグインを hot 登録するたびに bump し,キャッシュが自動的に失効します——毎回 registry をクエリするたびに byKey Map を再計算するのを避けます。

インデックス充填時は先着順(registry-lookup.ts:31)です:

typescript
function setLookupEntry(
  map: Map<string, RegisteredChannelPluginEntry>,
  key: string | undefined,
  entry: RegisteredChannelPluginEntry,
): void {
  // First writer wins so canonical ids keep priority over later aliases.
  if (key && !map.has(key)) {
    map.set(key, entry);
  }
}

このルールは正規 id が常に最初に登録されたものを指すことを保証し,後から登録された同名 alias は抢占できません——これは hot プラグイン可能シナリオで重要で,後からインストールしたプラグインが bundled チャネルを誤って上書きするのを防ぎます。

チャネルプラグイン本体の検索は getChannelPlugin で,まず loaded を探し,bundled にフォールバック(plugins/registry.ts:49)します:

typescript
export function getChannelPlugin(id: ChannelId): ChannelPlugin | undefined {
  const resolvedId = normalizeOptionalString(id) ?? "";
  if (!resolvedId) {
    return undefined;
  }
  // Loaded plugins win over bundled fallbacks so installed plugin state can pin
  // or override a bundled channel during runtime.
  return getLoadedChannelPlugin(resolvedId) ?? getBundledChannelPlugin(resolvedId);
}

bundled 読み込み失敗時はwarn だけでスローしない(plugins/bundled.ts:679)log.warn(\[channels] failed to load bundled channel ${id}: ${detail}`)してnull をキャッシュします。次回同じ id をクエリすると直接 undefined を返し,繰り返しロードをトリガーしません——これは「ランタイム依存欠落」のための容错空間で,describeBundledChannelLoadErrorはユーザにopenclaw doctor --fix` の実行を促します。

検索チェーン全体:

境界と失敗

  • スナップショットキャッシュの失効は version に依存:buildRegisteredChannelPluginLookupgetActivePluginChannelRegistrySnapshotFromStateversion を取ります。plugin registry が version を進めたのに channel state に通知し忘れると,hot 登録されたチャネルが見つかりません。
  • 先着順:別名は先着順です。2 つの plugin が同じ id を登録した場合,最初の 1 つだけが有効になり,後続は黙って無視されます——だからチャネル id 名の衝突はスローされず,「インストールされたように見えるが実は効いていない」状態になります。
  • bundled 読み込み失敗は warn だけ:missing module エラーは openclaw doctor --fix の実行を促します(bundled.ts:318)が,起動をブロックしません。これは 1 つのチャネルが壊れても残り 21 個に影響しないようにするためです。
  • bundled fallback はビルトイン 22 チャネルにだけ有効:サードパーティプラグインチャネルは generated metadata に入らず,loaded が未インストールなら getBundledChannelPlugin は直接 undefined を返します——兜底はありません。
  • runtime catalog 兜底:listRuntimeBundledChatChannelEntries??= で一度キャッシュ(ids.ts:61)し,初回呼び出しで catalog ファイルを読み,以降はメモリだけを使います。catalog ファイルがランタイムに置き換えられた場合,プロセスを再起動しないと感知できません。
  • 生成タイミング:GENERATED_BUNDLED_CHANNEL_CONFIG_METADATAscripts/generate-bundled-channel-config-metadata.ts がビルド時に生成し,新チャネル追加時に生成スクリプトを再実行しないと CHAT_CHANNEL_ID_SET に新 id が含まれません。

まとめ

登録表は「列挙、正規化、検索、紹介」の 4 件事だけを行い,チャネルランタイムには触れません。generated metadata + runtime catalog の 2 段階兜底で 22 個の bundled チャネルと任意のサードパーティプラグインチャネルをカバーし,スナップショット version キャッシュで毎回のクエリのインデックス再計算を避け,先着順で hot 登録が既存の正規 id を上書きしないことを保証します。

実際にチャネル能力を呼ぶ場所は チャネルアダプタ,ユーザが openclaw.json でこれらのチャネルをどう設定するかは チャネル設定,プラグインシステムの全体メカニズムは 能力プラグイン を参照。チャネルがどうメッセージを ゲートウェイコア に送って agent メインループを走らせ,再びアダプタ経由で送り返すかは次ページのテーマです。