Get started
채널 표시 방식 리팩터링 계획
상태
공유 에이전트, CLI, Plugin 기능, 아웃바운드 전달 표면에 구현됨:
ReplyPayload.presentation은 의미적 메시지 UI를 전달합니다.ReplyPayload.delivery.pin은 전송된 메시지 고정 요청을 전달합니다.- 공유 메시지 액션은 provider 네이티브
components,blocks,buttons,card대신presentation,delivery,pin을 노출합니다. - Core는 Plugin이 선언한 아웃바운드 기능을 통해 프레젠테이션을 렌더링하거나 자동으로 degrade합니다.
- Discord, Slack, Telegram, Mattermost, MS Teams, Feishu 렌더러는 generic contract를 사용합니다.
- Discord 채널 control-plane 코드는 더 이상 Carbon 기반 UI 컨테이너를 import하지 않습니다.
정식 문서는 이제 메시지 프레젠테이션에 있습니다. 이 계획은 과거 구현 맥락으로 유지하고, contract, 렌더러, fallback 동작 변경은 정식 가이드에서 업데이트하세요.
문제
채널 UI는 현재 여러 호환되지 않는 표면으로 나뉘어 있습니다:
- Core는
buildCrossContextComponents를 통해 Discord 형태의 cross-context 렌더러 hook을 소유합니다. - Discord
channel.ts는DiscordUiContainer를 통해 네이티브 Carbon UI를 import할 수 있으며, 이로 인해 런타임 UI 의존성이 채널 Plugin control plane으로 유입됩니다. - 에이전트와 CLI는 Discord
components, Slackblocks, Telegram 또는 Mattermostbuttons, Teams 또는 Feishucard같은 네이티브 payload escape hatch를 노출합니다. ReplyPayload.channelData는 transport 힌트와 네이티브 UI envelope을 모두 전달합니다.- generic
interactive모델이 존재하지만, Discord, Slack, Teams, Feishu, LINE, Telegram, Mattermost에서 이미 사용하는 더 풍부한 레이아웃보다 범위가 좁습니다.
이로 인해 Core가 네이티브 UI 형태를 인지하게 되고, Plugin 런타임 지연성이 약해지며, 에이전트가 같은 메시지 의도를 표현하는 provider별 방식을 너무 많이 갖게 됩니다.
목표
- Core는 선언된 기능을 바탕으로 메시지에 가장 적합한 의미적 프레젠테이션을 결정합니다.
- Extensions는 기능을 선언하고 의미적 프레젠테이션을 네이티브 transport payload로 렌더링합니다.
- Web Control UI는 채팅 네이티브 UI와 분리된 상태로 유지됩니다.
- 네이티브 채널 payload는 공유 에이전트 또는 CLI 메시지 표면을 통해 노출되지 않습니다.
- 지원되지 않는 프레젠테이션 기능은 최적의 텍스트 표현으로 자동 degrade됩니다.
- 전송된 메시지 고정 같은 전달 동작은 프레젠테이션이 아니라 generic 전달 메타데이터입니다.
비목표
buildCrossContextComponents에 대한 이전 버전 호환성 shim은 없습니다.components,blocks,buttons,card에 대한 public 네이티브 escape hatch는 없습니다.- Core가 채널 네이티브 UI 라이브러리를 import하지 않습니다.
- 번들 채널을 위한 provider별 SDK seam은 없습니다.
대상 모델
Core가 소유하는 presentation 필드를 ReplyPayload에 추가합니다.
type MessagePresentationTone = "neutral" | "info" | "success" | "warning" | "danger";
type MessagePresentation = {
tone?: MessagePresentationTone;
title?: string;
blocks: MessagePresentationBlock[];
};
type MessagePresentationBlock =
| { type: "text"; text: string }
| { type: "context"; text: string }
| { type: "divider" }
| { type: "buttons"; buttons: MessagePresentationButton[] }
| { type: "select"; placeholder?: string; options: MessagePresentationOption[] };
type MessagePresentationButton = {
label: string;
value?: string;
url?: string;
style?: "primary" | "secondary" | "success" | "danger";
};
type MessagePresentationOption = {
label: string;
value: string;
};
마이그레이션 중 interactive는 presentation의 subset이 됩니다:
interactive텍스트 block은presentation.blocks[].type = "text"에 매핑됩니다.interactive버튼 block은presentation.blocks[].type = "buttons"에 매핑됩니다.interactiveselect block은presentation.blocks[].type = "select"에 매핑됩니다.
외부 에이전트와 CLI 스키마는 이제 presentation을 사용합니다. interactive는 기존 reply producer를 위한 내부 legacy 파서/렌더링 helper로 남습니다.
전달 메타데이터
UI가 아닌 전송 동작을 위한 Core 소유 delivery 필드를 추가합니다.
type ReplyPayloadDelivery = {
pin?:
| boolean
| {
enabled: boolean;
notify?: boolean;
required?: boolean;
};
};
의미:
delivery.pin = true는 성공적으로 전달된 첫 번째 메시지를 고정한다는 뜻입니다.notify기본값은false입니다.required기본값은false입니다. 지원되지 않는 채널이나 고정 실패는 전달을 계속 진행하는 방식으로 자동 degrade됩니다.- 수동
pin,unpin,list-pins메시지 액션은 기존 메시지를 위해 유지됩니다.
현재 Telegram ACP 토픽 binding은 channelData.telegram.pin = true에서 delivery.pin = true로 이동해야 합니다.
런타임 기능 contract
control-plane 채널 Plugin이 아니라 runtime outbound adapter에 프레젠테이션 및 전달 렌더 hook을 추가합니다.
type ChannelPresentationCapabilities = {
supported: boolean;
buttons?: boolean;
selects?: boolean;
context?: boolean;
divider?: boolean;
tones?: MessagePresentationTone[];
};
type ChannelDeliveryCapabilities = {
pinSentMessage?: boolean;
};
type ChannelOutboundAdapter = {
presentationCapabilities?: ChannelPresentationCapabilities;
renderPresentation?: (params: {
payload: ReplyPayload;
presentation: MessagePresentation;
ctx: ChannelOutboundSendContext;
}) => ReplyPayload | null;
deliveryCapabilities?: ChannelDeliveryCapabilities;
pinDeliveredMessage?: (params: {
cfg: OpenClawConfig;
accountId?: string | null;
to: string;
threadId?: string | number | null;
messageId: string;
notify: boolean;
}) => Promise<void>;
};
Core 동작:
- 대상 채널과 런타임 adapter를 resolve합니다.
- 프레젠테이션 기능을 요청합니다.
- 렌더링 전에 지원되지 않는 block을 degrade합니다.
renderPresentation을 호출합니다.- 렌더러가 없으면 프레젠테이션을 텍스트 fallback으로 변환합니다.
- 성공적으로 전송한 뒤,
delivery.pin이 요청되고 지원되는 경우pinDeliveredMessage를 호출합니다.
채널 매핑
Discord:
- 런타임 전용 모듈에서
presentation을 components v2와 Carbon 컨테이너로 렌더링합니다. - accent color helper는 가벼운 모듈에 유지합니다.
- 채널 Plugin control-plane 코드에서
DiscordUiContainerimport를 제거합니다.
Slack:
presentation을 Block Kit으로 렌더링합니다.- 에이전트와 CLI
blocks입력을 제거합니다.
Telegram:
- 텍스트, context, divider를 텍스트로 렌더링합니다.
- 대상 표면에 대해 구성되고 허용된 경우 액션과 select를 inline keyboard로 렌더링합니다.
- inline button이 비활성화된 경우 텍스트 fallback을 사용합니다.
- ACP 토픽 고정을
delivery.pin으로 이동합니다.
Mattermost:
- 구성된 경우 액션을 interactive button으로 렌더링합니다.
- 다른 block은 텍스트 fallback으로 렌더링합니다.
MS Teams:
presentation을 Adaptive Cards로 렌더링합니다.- 수동 pin/unpin/list-pins 액션은 유지합니다.
- 대상 conversation에서 Graph 지원이 신뢰할 수 있으면 선택적으로
pinDeliveredMessage를 구현합니다.
Feishu:
presentation을 interactive card로 렌더링합니다.- 수동 pin/unpin/list-pins 액션은 유지합니다.
- API 동작이 신뢰할 수 있으면 전송된 메시지 고정을 위해 선택적으로
pinDeliveredMessage를 구현합니다.
LINE:
- 가능한 경우
presentation을 Flex 또는 template message로 렌더링합니다. - 지원되지 않는 block은 텍스트로 fallback합니다.
- LINE UI payload를
channelData에서 제거합니다.
Plain 또는 제한된 채널:
- 보수적인 서식으로 프레젠테이션을 텍스트로 변환합니다.
리팩터링 단계
ui-colors.ts를 Carbon 기반 UI에서 분리하고extensions/discord/src/channel.ts에서DiscordUiContainer를 제거하는 Discord release fix를 다시 적용합니다.ReplyPayload, 아웃바운드 payload normalization, 전달 summary, hook payload에presentation과delivery를 추가합니다.- 좁은 SDK/runtime subpath에
MessagePresentation스키마와 parser helper를 추가합니다. - 메시지 기능
buttons,cards,components,blocks를 의미적 프레젠테이션 기능으로 대체합니다. - 프레젠테이션 렌더링과 전달 고정을 위한 런타임 아웃바운드 adapter hook을 추가합니다.
- cross-context component 생성을
buildCrossContextPresentation으로 대체합니다. src/infra/outbound/channel-adapters.ts를 삭제하고 채널 Plugin type에서buildCrossContextComponents를 제거합니다.maybeApplyCrossContextMarker를 변경해 네이티브 params 대신presentation을 붙입니다.- Plugin-dispatch send path를 업데이트해 의미적 프레젠테이션과 전달 메타데이터만 사용하게 합니다.
- 에이전트와 CLI 네이티브 payload params인
components,blocks,buttons,card를 제거합니다. - 네이티브 message-tool 스키마를 만드는 SDK helper를 제거하고 프레젠테이션 스키마 helper로 대체합니다.
- UI/native envelope을
channelData에서 제거합니다. 남은 각 필드를 검토할 때까지 transport metadata만 유지합니다. - Discord, Slack, Telegram, Mattermost, MS Teams, Feishu, LINE 렌더러를 마이그레이션합니다.
- 메시지 CLI, 채널 페이지, Plugin SDK, 기능 cookbook 문서를 업데이트합니다.
- Discord 및 영향을 받는 채널 entrypoint에 대해 import fanout profiling을 실행합니다.
1-11단계와 13-14단계는 이 리팩터링에서 공유 에이전트, CLI, Plugin 기능, 아웃바운드 adapter contract에 구현되었습니다. 12단계는 provider-private channelData transport envelope에 대한 더 깊은 내부 cleanup pass로 남아 있습니다. 15단계는 type/test gate를 넘어 정량화된 import-fanout 수치를 원할 경우의 후속 validation으로 남아 있습니다.
테스트
추가 또는 업데이트:
- 프레젠테이션 normalization 테스트.
- 지원되지 않는 block에 대한 프레젠테이션 자동 degrade 테스트.
- Plugin dispatch와 Core 전달 path에 대한 cross-context marker 테스트.
- Discord, Slack, Telegram, Mattermost, MS Teams, Feishu, LINE, 텍스트 fallback에 대한 채널 렌더 matrix 테스트.
- 네이티브 필드가 제거되었음을 증명하는 메시지 tool 스키마 테스트.
- 네이티브 flag가 제거되었음을 증명하는 CLI 테스트.
- Carbon을 포함하는 Discord entrypoint import-laziness regression.
- Telegram 및 generic fallback을 포함하는 전달 고정 테스트.
열린 질문
delivery.pin을 첫 pass에서 Discord, Slack, MS Teams, Feishu까지 구현해야 하나요, 아니면 Telegram만 먼저 구현해야 하나요?delivery가 결국replyToId,replyToCurrent,silent,audioAsVoice같은 기존 필드도 흡수해야 하나요, 아니면 전송 후 동작에 집중한 상태로 유지해야 하나요?- 프레젠테이션이 이미지나 파일 참조를 직접 지원해야 하나요, 아니면 미디어는 지금은 UI 레이아웃과 분리된 상태로 남겨야 하나요?