Skip to main content
OpenClaw は、送信(アウトバウンド)される Markdown テキストを一度共有の中間表現 (IR) に変換してから、各チャネル固有の形式にレンダリングします。この IR は、元のテキストの内容を保持しつつスタイルやリンクの範囲情報を付随させることで、複数のチャネルにわたって一貫したチャンク化(分割)と表示を可能にします。

目標

  • 一貫性: 1 回のパース(解析)で、複数のレンダラーに対応。
  • 安全なチャンク化: レンダリング前にテキストを分割することで、太字などのスタイルが分割位置で壊れるのを防ぎます。
  • チャネルへの最適化: 同じ IR から、Slack の mrkdwn、Telegram の HTML、Signal のスタイル範囲へと、Markdown を再解析することなくマッピングします。

パイプライン

  1. Markdown -> IR の解析
    • IR は、プレーンテキストにスタイル範囲(太字/斜体/打ち消し線/コード/ネタバレ)とリンク範囲を組み合わせたデータ構造です。
    • オフセット値は UTF-16 コード単位で保持されるため、Signal のスタイル範囲指定とも正確に一致します。
    • テーブル(表)の解析は、そのチャネルがテーブル変換を有効にしている場合にのみ行われます。
  2. IR のチャンク化 (フォーマット優先)
    • レンダリングを行う前の IR 段階でテキストを分割します。
    • インラインのスタイル指定が分割位置をまたぐことはありません。スタイル範囲はチャンクごとに適切に切り分けられます。
  3. チャネルごとのレンダリング
    • Slack: mrkdwn トークン(太字/斜体/打ち消し線/コード)、リンクは <url|ラベル> 形式。
    • Telegram: HTML タグ (<b>, <i>, <s>, <code>, <pre><code>, <a href>)。
    • Signal: プレーンテキスト + text-style 範囲指定。リンクはラベルが URL と異なる場合に ラベル (url) 形式になります。

IR の例

入力 Markdown:
こんにちは **世界**[ドキュメント](https://docs.openclaw.ai) を見てください。
IR (イメージ図):
{
  "text": "こんにちは 世界 — ドキュメント を見てください。",
  "styles": [{ "start": 6, "end": 8, "style": "bold" }],
  "links": [{ "start": 11, "end": 17, "href": "https://docs.openclaw.ai" }]
}

利用されている場所

  • Slack, Telegram, Signal のアウトバウンド用アダプターは、この IR を元にメッセージを生成します。
  • 他のチャネル (WhatsApp, iMessage, MS Teams, Discord) は、現在もプレーンテキストまたは独自のフォーマットルールを使用していますが、有効な場合はチャンク化の前に Markdown テーブルの変換が適用されます。

テーブル(表)の扱い

Markdown のテーブルは、チャットクライアントによってサポート状況が大きく異なります。markdown.tables 設定を使用して、チャネルごと(あるいはアカウントごと)に変換方法を制御できます。
  • code: テーブルをコードブロックとしてレンダリングします(ほとんどのチャネルのデフォルト)。
  • bullets: 各行を箇条書き(弾丸リスト)に変換します(Signal, WhatsApp のデフォルト)。
  • off: テーブルの解析と変換を無効にします。元のテーブルテキストがそのまま送信されます。
構成例:
channels:
  discord:
    markdown:
      tables: code
    accounts:
      work:
        markdown:
          tables: off

チャンク化(分割)のルール

  • チャンクの文字数制限はチャネルアダプターや構成から取得され、IR のプレーンテキストに対して適用されます。
  • コードブロック(Code fence)は、末尾の改行と共に 1 つのブロックとして保持され、チャネル側で正しく表示されるよう配慮されます。
  • リストの接頭辞や引用(blockquote)の接頭辞は IR テキストの一部として扱われるため、接頭辞の途中で分割されることはありません。
  • インラインスタイル(太字/斜体/打ち消し線/インラインコード/ネタバレ)がチャンクをまたいで分割されることはありません。レンダラーは、各チャンクの開始時にスタイルを再開(開き直し)します。
チャネルごとのストリーミング時の分割動作については、ストリーミングとチャンク化 を参照してください。

リンクのポリシー

  • Slack: [ラベル](url) -> <url|ラベル>。URL 単体はそのまま保持されます。二重リンクを防ぐため、解析中の自動リンク機能はオフになっています。
  • Telegram: [ラベル](url) -> <a href="url">ラベル</a> (HTML パースモード)。
  • Signal: [ラベル](url) -> ラベルが URL と一致しない限り ラベル (url)

ネタバレ (Spoilers)

ネタバレマーカー (||テキスト||) は、Signal 用にのみ解析され、SPOILER スタイル範囲にマップされます。他のチャネルではプレーンテキストとして扱われます。

新しいチャネルフォーマッタの追加手順

  1. 一度だけパース: 共有ヘルパー markdownToIR(...) を使用します。チャネルに適したオプション(自動リンク、見出しスタイル、引用接頭辞など)を指定してください。
  2. レンダリング: renderMarkdownWithMarkers(...) とスタイルマーカーのマッピング(または Signal 用のスタイル範囲)を実装します。
  3. チャンク化: レンダリングの前に chunkMarkdownIR(...) を呼び出し、分割された各チャンクをレンダリングします。
  4. アダプターへの接続: チャネルのアウトバウンドアダプターを更新し、新しいチャンカーとレンダラーを使用するように変更します。
  5. テスト: フォーマットテストを追加・更新します。チャンク化を使用するチャネルの場合は、配信テストも追加してください。

よくある落とし穴

  • Slack の角括弧トークン (<@U123>, <#C123>, <https://...>) は保持される必要があります。生身の HTML は安全にエスケープしてください。
  • Telegram の HTML モードでは、タグ以外のテキストをエスケープしないと表示が壊れる原因になります。
  • Signal のスタイル範囲は UTF-16 オフセットに基づいています。コードポイントベースのオフセットを使用しないでください。
  • フェンスで囲まれたコードブロックの末尾の改行を保持してください。そうしないと、閉じマーカーが専用の行に配置されない可能性があります。