마크다운 포매팅

OpenClaw는 아웃바운드 마크다운을 공유 중간 표현(IR)으로 변환한 후 채널별 출력을 렌더링합니다. IR은 원본 텍스트를 그대로 유지하면서 스타일/링크 스팬을 포함하여, 청킹과 렌더링이 채널 간에 일관되게 동작할 수 있게 합니다.

목표

  • 일관성: 한 번의 파싱, 여러 렌더러.
  • 안전한 청킹: 렌더링 전에 텍스트를 분할하여 인라인 포매팅이 청크 경계에서 깨지지 않도록 합니다.
  • 채널 적합성: 마크다운을 다시 파싱하지 않고 동일한 IR을 Slack mrkdwn, Telegram HTML, Signal 스타일 범위에 매핑합니다.

파이프라인

  1. 마크다운 -> IR 파싱
    • IR은 일반 텍스트 + 스타일 스팬(볼드/이탤릭/취소선/코드/스포일러) + 링크 스팬으로 구성됩니다.
    • Signal 스타일 범위가 API에 맞도록 오프셋은 UTF-16 코드 유닛입니다.
    • 테이블은 채널이 테이블 변환을 선택한 경우에만 파싱됩니다.
  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 예시

입력 마크다운:

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.tables로 채널별(그리고 계정별) 변환을 제어하세요.

  • code: 테이블을 코드 블록으로 렌더링 (대부분의 채널 기본값).
  • bullets: 각 행을 글머리 기호로 변환 (Signal + WhatsApp 기본값).
  • off: 테이블 파싱과 변환을 비활성화. 원본 테이블 텍스트가 그대로 통과.

설정 키:

channels:
  discord:
    markdown:
      tables: code
    accounts:
      work:
        markdown:
          tables: off

청킹 규칙

  • 청크 제한은 채널 어댑터/설정에서 가져와 IR 텍스트에 적용됩니다.
  • 코드 펜스는 채널이 올바르게 렌더링할 수 있도록 후행 줄바꿈과 함께 단일 블록으로 유지됩니다.
  • 목록 접두사와 인용문 접두사는 IR 텍스트의 일부이므로 접두사 중간에서 분할되지 않습니다.
  • 인라인 스타일(볼드/이탤릭/취소선/인라인코드/스포일러)은 청크 간에 절대 분할되지 않습니다. 렌더러가 각 청크 내에서 스타일을 다시 엽니다.

채널 간 청킹 동작에 대한 자세한 내용은 스트리밍 + 청킹을 참고하세요.

링크 정책

  • Slack: [label](/docs/concepts/url) -> <url|label>. 단독 URL은 그대로 유지. 이중 링크 방지를 위해 파싱 시 자동 링크가 비활성화됩니다.
  • Telegram: [label](/docs/concepts/url) -> <a href="url">label</a> (HTML 파싱 모드).
  • Signal: [label](/docs/concepts/url) -> label (url) (레이블이 URL과 같으면 제외).

스포일러

스포일러 마커(||spoiler||)는 Signal에서만 파싱되며, SPOILER 스타일 범위에 매핑됩니다. 다른 채널에서는 일반 텍스트로 처리됩니다.

채널 포매터 추가 또는 업데이트 방법

  1. 한 번 파싱: 공유 markdownToIR(...) 헬퍼를 채널에 적합한 옵션(자동 링크, 제목 스타일, 인용문 접두사)과 함께 사용합니다.
  2. 렌더링: renderMarkdownWithMarkers(...)와 스타일 마커 맵(또는 Signal 스타일 범위)으로 렌더러를 구현합니다.
  3. 청킹: 렌더링 전에 chunkMarkdownIR(...)를 호출합니다. 각 청크를 렌더링합니다.
  4. 어댑터 연결: 채널 아웃바운드 어댑터를 새 청커와 렌더러를 사용하도록 업데이트합니다.
  5. 테스트: 포맷 테스트를 추가하거나 업데이트하고, 채널이 청킹을 사용하는 경우 아웃바운드 전달 테스트도 추가합니다.

주의사항

  • Slack 꺾쇠 괄호 토큰(<@U123>, <#C123>, <https://...>)을 보존해야 합니다. 원시 HTML을 안전하게 이스케이프하세요.
  • Telegram HTML은 태그 외부의 텍스트를 이스케이프해야 마크업 손상을 방지할 수 있습니다.
  • Signal 스타일 범위는 UTF-16 오프셋에 의존합니다. 코드 포인트 오프셋을 사용하지 마세요.
  • 닫는 마커가 자체 줄에 오도록 펜스된 코드 블록의 후행 줄바꿈을 유지하세요.