メッセージルーティングとセキュリティパイプライン
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 はセッターメソッドを通じてセキュリティサービスを注入します。各セキュリティレイヤーはオプションです:
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. トリガーパターンチェック
├── config なし または 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 インスタンス + チャットセッションごとに1つのバッファ)
バッファの動作:
- 各バッファは最大 50 件のメッセージを保持(FIFO 方式で追い出し)
- トリガー時、バッファ内の全メッセージ + トリガーメッセージが Agent に送信されます
- 送信後、バッファはクリアされます
clearBuffers()で Router がシャットダウンされると、すべてのバッファがクリアされます
メッセージフォーマット
グループメッセージは 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 — プラグインインターフェース定義