메인 콘텐츠로 건너뛰기

Markdown 서식

OpenClaw 는 아웃바운드 Markdown 을 채널별 출력으로 렌더링하기 전에 공통의 중간 표현(IR)으로 변환하여 서식을 처리합니다. IR 은 소스 텍스트를 그대로 유지하면서 스타일/링크 스팬을 포함하므로, 청킹과 렌더링을 채널 전반에서 일관되게 유지할 수 있습니다.

목표

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

파이프라인

  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 범위; 레이블이 다를 경우 링크는 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](url) -> <url|label>; 순수 URL 은 그대로 유지됩니다. 이중 링크를 방지하기 위해 파싱 중 자동 링크는 비활성화됩니다.
  • Telegram: [label](url) -> <a href="url">label</a>(HTML 파싱 모드).
  • Signal: 레이블이 URL 과 일치하지 않는 경우 [label](url) -> label (url).

스포일러

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

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

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

자주 발생하는 함정

  • Slack 각도 괄호 토큰(<@U123>, <#C123>, <https://...>)은 반드시 보존해야 하며, 원시 HTML 은 안전하게 이스케이프해야 합니다.
  • Telegram HTML 은 태그 외부의 텍스트를 이스케이프하지 않으면 마크업이 깨질 수 있습니다.
  • Signal 스타일 범위는 UTF-16 오프셋에 의존하므로 코드 포인트 오프셋을 사용하지 마십시오.
  • 펜스 코드 블록의 닫는 마커가 자체 줄에 위치하도록 끝 개행을 보존하십시오.