마크다운 포매팅
OpenClaw는 아웃바운드 마크다운을 공유 중간 표현(IR)으로 변환한 후 채널별 출력을 렌더링합니다. IR은 원본 텍스트를 그대로 유지하면서 스타일/링크 스팬을 포함하여, 청킹과 렌더링이 채널 간에 일관되게 동작할 수 있게 합니다.
목표
- 일관성: 한 번의 파싱, 여러 렌더러.
- 안전한 청킹: 렌더링 전에 텍스트를 분할하여 인라인 포매팅이 청크 경계에서 깨지지 않도록 합니다.
- 채널 적합성: 마크다운을 다시 파싱하지 않고 동일한 IR을 Slack mrkdwn, Telegram HTML, Signal 스타일 범위에 매핑합니다.
파이프라인
- 마크다운 -> IR 파싱
- IR은 일반 텍스트 + 스타일 스팬(볼드/이탤릭/취소선/코드/스포일러) + 링크 스팬으로 구성됩니다.
- Signal 스타일 범위가 API에 맞도록 오프셋은 UTF-16 코드 유닛입니다.
- 테이블은 채널이 테이블 변환을 선택한 경우에만 파싱됩니다.
- IR 청킹 (포맷 우선)
- 렌더링 전에 IR 텍스트에서 청킹이 발생합니다.
- 인라인 포매팅은 청크 간에 분할되지 않습니다. 스팬은 청크별로 슬라이스됩니다.
- 채널별 렌더링
- Slack: mrkdwn 토큰 (볼드/이탤릭/취소선/코드), 링크는
<url|label>. - Telegram: HTML 태그 (
<b>,<i>,<s>,<code>,<pre><code>,<a href>). - Signal: 일반 텍스트 +
text-style범위. 레이블이 URL과 다를 때 링크는label (url).
- Slack: mrkdwn 토큰 (볼드/이탤릭/취소선/코드), 링크는
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 스타일 범위에 매핑됩니다. 다른 채널에서는 일반 텍스트로 처리됩니다.
채널 포매터 추가 또는 업데이트 방법
- 한 번 파싱: 공유
markdownToIR(...)헬퍼를 채널에 적합한 옵션(자동 링크, 제목 스타일, 인용문 접두사)과 함께 사용합니다. - 렌더링:
renderMarkdownWithMarkers(...)와 스타일 마커 맵(또는 Signal 스타일 범위)으로 렌더러를 구현합니다. - 청킹: 렌더링 전에
chunkMarkdownIR(...)를 호출합니다. 각 청크를 렌더링합니다. - 어댑터 연결: 채널 아웃바운드 어댑터를 새 청커와 렌더러를 사용하도록 업데이트합니다.
- 테스트: 포맷 테스트를 추가하거나 업데이트하고, 채널이 청킹을 사용하는 경우 아웃바운드 전달 테스트도 추가합니다.
주의사항
- Slack 꺾쇠 괄호 토큰(
<@U123>,<#C123>,<https://...>)을 보존해야 합니다. 원시 HTML을 안전하게 이스케이프하세요. - Telegram HTML은 태그 외부의 텍스트를 이스케이프해야 마크업 손상을 방지할 수 있습니다.
- Signal 스타일 범위는 UTF-16 오프셋에 의존합니다. 코드 포인트 오프셋을 사용하지 마세요.
- 닫는 마커가 자체 줄에 오도록 펜스된 코드 블록의 후행 줄바꿈을 유지하세요.