메시지 라우팅 및 보안 파이프라인
ChannelMessageRouter는 Channel 시스템의 핵심 허브로, 모든 Channel 플러그인에서 들어오는 인바운드 메시지를 수신하고, 보안 파이프라인을 통해 처리하며, 트리거 규칙에 매칭하고, Agent 처리로 라우팅하며, 응답을 원래 플랫폼에 맞게 포매팅하여 반환하는 역할을 담당합니다.
소스 위치: packages/desktop/app/main/services/capabilities/integrations/channel/ChannelMessageRouter.ts
전체 처리 파이프라인
sequenceDiagram
participant Plugin as Channel Plugin
participant Registry as ChannelPluginRegistry
participant Router as ChannelMessageRouter
participant RL as RateLimiter
participant IS as InputSanitizer
participant PG as PromptGuardian
participant UP as UserPermissionService
participant Gate as ChannelPermissionGate
participant Bridge as ChannelMagiBridge
participant Agent as MagiService
Plugin->>Registry: emitMessage(InboundMessage)
Registry->>Router: emit('message', ChannelMessage)
Note over Router: 보안 파이프라인 시작
Router->>RL: check(channelId, senderId)
alt 속도 제한 초과
RL-->>Router: { allowed: false }
Note over Router: 조용히 폐기
end
Router->>IS: sanitize(content)
IS-->>Router: { content, modified, warnings }
Note over Router: 정제된 내용으로 교체
Router->>PG: review(content, context)
alt 인젝션 감지됨
PG-->>Router: { allowed: false, deflectionMessage }
Router->>Registry: sendMessage(deflectionMessage)
Note over Router: 메시지 차단됨
end
Router->>UP: checkPermission(channelId, senderId, name)
alt 사용자가 차단됨
UP-->>Router: { allowed: false, role: 'blocked' }
Router->>Registry: sendMessage("You have been blocked")
Note over Router: 메시지 차단됨
end
Router->>Gate: processReply(channelId, chatId, senderId, content)
alt 권한 확인 응답인 경우
Gate-->>Router: consumed = true
Note over Router: 확인 메커니즘에 의해 메시지 소비됨
end
Note over Router: 보안 파이프라인 종료, 트리거 매칭 진입
Router->>Router: shouldTrigger(msg, config)
alt 트리거 규칙 미매칭
Note over Router: bufferMessage (최대 50개)
else 트리거 규칙 매칭
Router->>Registry: emit('routeToAgent', payload)
Bridge->>Agent: handleMessage(incoming)
Agent-->>Bridge: { response }
Bridge->>Router: sendResponse(channelId, chatId, text, type)
Router->>Router: stripInternalTags + splitMessage
Router->>Registry: sendMessage(chunk)
Registry->>Plugin: plugin.sendMessage(chatId, chunk)
end
보안 서비스 주입
Router는 setter 메서드를 통해 보안 서비스를 주입합니다. 각 보안 레이어는 선택 사항입니다:
setRateLimiter(limiter: RateLimiter): void
setSanitizer(sanitizer: InputSanitizer): void
setPromptGuardian(guardian: PromptGuardian): void
setUserPermissions(service: UserPermissionService): void
setPermissionGate(gate: ChannelPermissionGate): void
주입되지 않은 보안 레이어는 건너뜁니다. 이를 통해 다양한 배포 시나리오에 대해 유연한 보안 설정이 가능합니다.
트리거 매칭 로직
shouldTrigger(msg, config)의 결정 흐름:
1. allowFrom 확인 (모든 패턴에 공통)
├── allowFrom이 비어 있거나 "*"를 포함 → 통과
├── senderId가 화이트리스트에 있음 → 통과
└── senderId가 화이트리스트에 없음 → false 반환 (트리거 안 함, 캐시 안 함)
2. 다이렉트 메시지 확인
└── !msg.isGroup → true 반환 (DM은 항상 트리거)
3. 트리거 패턴 확인
├── 설정 없음 또는 mode === 'all' → true
├── ignoreBot && msg.isFromMe → false
├── mode === 'dm_only' → !msg.isGroup
├── mode === 'mention' → content.includes(`@${mentionName}`)
└── mode === 'keyword' → keywords.some(kw => content.toLowerCase().includes(kw.toLowerCase()))
주요 세부 사항:
allowFrom확인은 트리거 패턴보다 우선하며 DM과 그룹 모두에 적용됩니다- DM은 항상 트리거됩니다 (
allowFrom확인 후), 트리거 패턴의 영향을 받지 않습니다 ignoreBot확인은 트리거 패턴 매칭 전에 수행됩니다- 키워드 매칭은 대소문자를 구분하지 않습니다
mentionName의 기본값은'Clawia'입니다
메시지 버퍼링
트리거되지 않은 메시지는 폐기되지 않고 메모리에 캐시됩니다:
private messageBuffer = new Map<string, ChannelMessage[]>();
버퍼 키: ${channelId}:${chatId} (Channel 인스턴스 + 채팅 세션별로 하나의 버퍼)
버퍼 동작:
- 각 버퍼는 최대 50개의 메시지를 보유 (FIFO 방식으로 제거)
- 트리거 시 버퍼의 모든 메시지 + 트리거 메시지가 Agent로 전송됨
- 전송 후 버퍼 초기화
- Router가
clearBuffers()를 통해 종료될 때 모든 버퍼 초기화
메시지 포매팅
그룹 메시지는 Agent로 전송되기 전에 XML 형식으로 래핑됩니다:
<channel_messages>
<message sender="Alice" channel="discord" channel_id="ch-001" chat="general" time="2026-03-19T10:00:00Z">Hello</message>
<message sender="Bob" channel="discord" channel_id="ch-001" chat="general" time="2026-03-19T10:00:05Z">Hi!</message>
<message sender="Alice" channel="discord" channel_id="ch-001" chat="general" time="2026-03-19T10:00:10Z">@Clawia can you help with this</message>
</channel_messages>
DM/다이렉트 메시지는 XML로 래핑되지 않고 프롬프트에 원시 텍스트로 직접 전송됩니다.
응답 처리
내부 태그 제거
Agent 응답의 <internal>...</internal> 태그가 제거됩니다:
function stripInternalTags(text: string): string {
return text.replace(/<internal>[\s\S]*?<\/internal>/g, '').trim();
}
이를 통해 Agent가 내부 용도로만 사용하는 메타데이터를 포함하면서도 외부 플랫폼으로 유출되지 않도록 합니다.
메시지 분할
플랫폼 메시지 길이 제한을 초과하는 응답은 자동으로 여러 메시지로 분할됩니다:
function splitMessage(text: string, maxLength: number): string[]
분할 전략 (우선순위 높은 순):
- 줄 바꿈 지점에서 분리
- 공백 지점에서 분리
maxLength에서 강제 분리
플랫폼별 길이 제한
ChannelMessageRouter는 각 플랫폼의 메시지 길이 제한을 유지합니다:
| 플랫폼 | 최대 길이 |
|---|---|
| discord | 2,000 |
| telegram | 4,096 |
| slack | 4,000 |
| qqbot | 1,500 (타입 정의) / 2,000 (Router 기본값) |
| line | 5,000 |
| 100,000 | |
| 65,000 | |
| matrix | 65,536 |
| msteams | 28,000 |
| mattermost | 16,383 |
| twitch | 500 |
| irc | 512 |
플러그인 매니페스트에 maxMessageLength가 선언된 경우 기본값을 재정의합니다.
첨부 파일 전송
Router는 Channel로 첨부 파일 전송도 지원합니다:
async sendAttachment(
channelId: string,
chatId: string,
attachment: AttachmentInput,
): Promise<void>
호출 체인: Router.sendAttachment() → Registry.sendAttachment() → plugin.sendAttachment(). 플러그인이 sendAttachment를 구현하지 않은 경우 오류가 발생합니다.
설정 관리
// 트리거 설정 설정/업데이트
setTriggerConfig(channelId: string, config: ChannelTriggerConfig): void
removeTriggerConfig(channelId: string): void
// 플랫폼 메시지 길이 제한 설정
setMaxLength(pluginType: string, maxLength: number): void
// 모든 메시지 버퍼 초기화
clearBuffers(): void
다음 단계
- ChannelMagiBridge — Router에서 Agent로 메시지가 흐르는 방식
- Channel Plugin SDK — 플러그인 인터페이스 정의