Markdownフォーマッティング

OpenClawは送信Markdownを共通の中間表現(IR)に変換してから、チャネルごとの出力をレンダリングします。IRはソーステキストをそのまま保持しつつスタイル/リンクスパンを付加するため、チャンキングとレンダリングがチャネル間で一貫した動作を維持できます。

目標

  • 一貫性: 1回のパースで複数のレンダラーに対応。
  • 安全なチャンキング: レンダリング前にテキストを分割するため、インラインフォーマットがチャンク境界で壊れない。
  • チャネル適合: 同じIRをSlackのmrkdwn、TelegramのHTML、Signalのスタイル範囲にマッピングし、Markdownの再パースを回避。

パイプライン

  1. Markdown → IRへのパース
    • IRはプレーンテキスト+スタイルスパン(太字/斜体/取り消し線/コード/スポイラー)+リンクスパン。
    • オフセットはUTF-16コードユニットで、Signalのスタイル範囲がそのAPIと一致するようになっています。
    • テーブルはチャネルがテーブル変換をオプトインした場合のみパースされます。
  2. IRのチャンキング(フォーマット優先)
    • チャンキングはレンダリング前にIRテキスト上で行われます。
    • インラインフォーマットはチャンク間で分割されず、スパンはチャンクごとにスライスされます。
  3. チャネルごとのレンダリング
    • Slack: mrkdwnトークン(太字/斜体/取り消し線/コード)、リンクは <url|label>
    • Telegram: HTMLタグ(<b><i><s><code><pre><code><a href>)。
    • Signal: プレーンテキスト+text-style範囲。リンクはラベルがURLと異なる場合 label (url) になります。

IRの例

入力Markdown:

Hello **world** — see [docs](https://docs.openclaw.ai).

IR(概略):

{
  "text": "Hello world — see docs.",
  "styles": [{ "start": 6, "end": 11, "style": "bold" }],
  "links": [{ "start": 19, "end": 23, "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テキストに適用されます。
  • コードフェンスは末尾改行付きの単一ブロックとして保持され、チャネルが正しくレンダリングできるようにします。
  • リスト接頭辞やブロック引用接頭辞はIRテキストの一部であるため、チャンキングが接頭辞の途中で分割することはありません。
  • インラインスタイル(太字/斜体/取り消し線/インラインコード/スポイラー)はチャンク間で分割されず、レンダラーが各チャンク内でスタイルを再開します。

チャネル間のチャンキング動作の詳細については、Streaming + chunkingを参照してください。

リンクポリシー

  • Slack: [label](/docs/concepts/url)<url|label>。裸のURLはそのまま。パース中のオートリンクは二重リンクを防ぐため無効化。
  • Telegram: [label](/docs/concepts/url)<a href="url">label</a>(HTMLパースモード)。
  • Signal: [label](/docs/concepts/url) → ラベルがURLと一致しない場合 label (url)

スポイラー

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

チャネルフォーマッターの追加・更新方法

  1. 1回パース: チャネルに適したオプション(オートリンク、見出しスタイル、ブロック引用接頭辞)で共有の markdownToIR(...) ヘルパーを使用。
  2. レンダリング: renderMarkdownWithMarkers(...) とスタイルマーカーマップ(またはSignalスタイル範囲)でレンダラーを実装。
  3. チャンキング: レンダリング前に chunkMarkdownIR(...) を呼び出し、各チャンクをレンダリング。
  4. アダプター接続: チャネル送信アダプターを更新して新しいチャンカーとレンダラーを使用。
  5. テスト: フォーマットテストを追加または更新し、チャネルがチャンキングを使用する場合は送信配信テストも追加。

よくある落とし穴

  • Slackの角括弧トークン(<@U123><#C123><https://...>)は保持する必要があります。生のHTMLは安全にエスケープしてください。
  • Telegram HTMLではタグ外のテキストをエスケープしないとマークアップが壊れます。
  • Signalのスタイル範囲はUTF-16オフセットに依存します。コードポイントオフセットを使用しないでください。
  • フェンスドコードブロックの閉じマーカーが独立した行に来るよう、末尾改行を保持してください。