Перейти к основному содержимому

Маршрутизация сообщений и конвейер безопасности

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[]

Стратегия разбивки (по убыванию приоритета):

  1. Разрыв по символам новой строки
  2. Разрыв по пробелам
  3. Жёсткий обрыв на maxLength

Ограничения длины сообщений по платформам

ChannelMessageRouter поддерживает ограничения длины сообщений для каждой платформы:

ПлатформаМаксимальная длина
discord2 000
telegram4 096
slack4 000
qqbot1 500 (в определении типа) / 2 000 (значение по умолчанию в Router)
line5 000
email100 000
whatsapp65 000
matrix65 536
msteams28 000
mattermost16 383
twitch500
irc512

Если в манифесте плагина объявлен 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 — определения интерфейсов плагинов