Маршрутизация сообщений и конвейер безопасности
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: Security pipeline starts
Router->>RL: check(channelId, senderId)
alt Rate limit exceeded
RL-->>Router: { allowed: false }
Note over Router: Silently discard
end
Router->>IS: sanitize(content)
IS-->>Router: { content, modified, warnings }
Note over Router: Replace with sanitized content
Router->>PG: review(content, context)
alt Injection detected
PG-->>Router: { allowed: false, deflectionMessage }
Router->>Registry: sendMessage(deflectionMessage)
Note over Router: Message blocked
end
Router->>UP: checkPermission(channelId, senderId, name)
alt User is blocked
UP-->>Router: { allowed: false, role: 'blocked' }
Router->>Registry: sendMessage("You have been blocked")
Note over Router: Message blocked
end
Router->>Gate: processReply(channelId, chatId, senderId, content)
alt Is permission confirmation reply
Gate-->>Router: consumed = true
Note over Router: Message consumed by confirmation mechanism
end
Note over Router: Security pipeline ends, enter trigger matching
Router->>Router: shouldTrigger(msg, config)
alt Trigger rules not matched
Note over Router: bufferMessage (up to 50)
else Trigger rules matched
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 внедряет сервисы безопасности через методы-сеттеры; каждый уровень безопасности является необязательным:
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 check (common to all patterns)
├── allowFrom is empty or contains "*" → pass
├── senderId in whitelist → pass
└── senderId not in whitelist → return false (no trigger, no cache)
2. Direct message check
└── !msg.isGroup → return true (DMs always trigger)
3. Trigger pattern check
├── No config or 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имеет приоритет над шаблонами триггеров и применяется как к личным сообщениям, так и к группам - Личные сообщения всегда вызывают триггер (после проверки
allowFrom) и не зависят от шаблонов триггеров - Проверка
ignoreBotвыполняется перед сопоставлением шаблонов триггеров - Сопоставление ключевых слов не чувствительно к регистру
- По умолчанию
mentionNameимеет значение'Clawia'
Буферизация сообщений
Сообщения, не вызвавшие триггер, не удаляются, а кэшируются в памяти:
private messageBuffer = new Map<string, ChannelMessage[]>();
Ключ буфера: ${channelId}:${chatId} (один буфер на экземпляр Channel + сессию чата)
Поведение буфера:
- Каждый буфер вмещает не более 50 сообщений (вытеснение по принципу FIFO)
- При срабатывании триггера все сообщения из буфера вместе с триггерным сообщением отправляются Agent
- После отправки буфер очищается
- При завершении работы Router все буферы очищаются через
clearBuffers()
Форматирование сообщений
Сообщения из групп оборачиваются в XML-формат перед отправкой Agent:
<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; они передаются напрямую в виде обычного текста в промпте.
Обработка ответов
Удаление внутренних тегов
Теги <internal>...</internal> в ответах Agent удаляются:
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, выбрасывается ошибка.
Управление конфигурацией
// Set/update trigger configuration
setTriggerConfig(channelId: string, config: ChannelTriggerConfig): void
removeTriggerConfig(channelId: string): void
// Set platform message length limit
setMaxLength(pluginType: string, maxLength: number): void
// Clear all message buffers
clearBuffers(): void
Дальнейшие шаги
- ChannelMagiBridge — как сообщения поступают от Router к Agent
- Channel Plugin SDK — определения интерфейсов плагинов