# 通道适配器

LibreFang 通过 **45 个通道适配器** 连接各类消息平台，让用户可以在所有主流通信平台上与 Agent 交互。适配器覆盖消费级即时通讯、企业协作、社交媒体、社区平台、隐私优先协议、通用 Webhook 以及可扩展的 Sidecar 进程。

所有适配器共享统一的基础能力：通过 `watch::channel` 实现优雅关闭、连接失败时指数退避重试、使用 `Zeroizing<String>` 保护密钥、超出平台长度限制时自动拆分消息、支持按通道覆盖模型/提示词、DM/群组策略控制、按用户速率限制，以及输出格式化（Markdown、TelegramHTML、SlackMrkdwn、PlainText）。

## 目录

- [核心消息渠道](/zh/integrations/channels/core) -- Telegram、Discord、Slack、WhatsApp、微信、Signal、Matrix、Email、WebChat
- [企业协作](/zh/integrations/channels/enterprise) -- Teams、Mattermost、Google Chat、Webex、飞书、Zulip、Flock、Twist、Pumble、Guilded
- [社交与社区](/zh/integrations/channels/social) -- Reddit、Mastodon、Bluesky、Twitch、Gitter、Revolt、Keybase
- [集成与协议](/zh/integrations/channels/integrations) -- LINE、Threema、钉钉、QQ、企业微信、IRC、XMPP、Ntfy、Gotify、Nextcloud、Mumble、Webhook、Voice
- [自定义适配器](/zh/integrations/channels/custom) -- 编写自定义适配器

---

## 通道配置

<Note>
所有通道凭证支持四种存储方式（优先级从高到低）：
- **系统环境变量**: `export TELEGRAM_BOT_TOKEN=...`
- **加密金库**（推荐）: `librefang vault set TELEGRAM_BOT_TOKEN`
- **.env 文件**: `librefang config set-key telegram` 或直接编辑 `~/.librefang/.env`
- **secrets.env**: 通过仪表板 "Set API Key" 按钮写入 `~/.librefang/secrets.env`

以下示例展示全部四种方式。生产环境建议使用 vault。
</Note>

大多数通道配置位于 `~/.librefang/config.toml` 的 `[channels]` 段落下，每个内置通道是一个子段落。Telegram 现已改为 sidecar 模式，使用 `[[sidecar_channels]]` 声明：

```toml
# Telegram（自 #5241 起改为 sidecar 模式）
[[sidecar_channels]]
name = "telegram"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.telegram"]
channel_type = "telegram"
[sidecar_channels.env]
TELEGRAM_BOT_TOKEN = "..."
# ALLOWED_USERS = "123456789,@alice"

# Discord (sidecar)
[[sidecar_channels]]
name = "discord"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.discord"]
channel_type = "discord"
[sidecar_channels.env]
DISCORD_BOT_TOKEN = "..."

# Slack (sidecar)
[[sidecar_channels]]
name = "slack"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.slack"]
channel_type = "slack"
[sidecar_channels.env]
SLACK_APP_TOKEN = "xapp-..."
SLACK_BOT_TOKEN = "xoxb-..."

# 企业示例（Teams — sidecar）
[[sidecar_channels]]
name = "teams"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.teams"]
channel_type = "teams"
[sidecar_channels.env]
TEAMS_APP_ID = "00000000-0000-0000-0000-000000000000"
TEAMS_WEBHOOK_PORT = "8459"

# 社交示例（Mastodon — sidecar）
[[sidecar_channels]]
name = "mastodon"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.mastodon"]
channel_type = "mastodon"
[sidecar_channels.env]
MASTODON_INSTANCE_URL = "https://mastodon.social"
```

### 通用字段

- `bot_token_env` / `token_env` -- 保存 Bot/访问令牌的环境变量名。LibreFang 在启动时从该环境变量读取令牌。所有密钥以 `Zeroizing<String>` 存储，释放时自动清零。
- `default_agent` -- 当没有特定路由规则时，接收消息的 Agent 名称（或 ID）。
- `allowed_users` -- 可选的平台用户 ID 白名单。留空表示允许所有用户。
- `overrides` -- 可选的按通道行为覆盖（参见下方[通道覆盖](#channel-overrides)）。

### 环境变量参考（核心通道）

| 通道 | 必需的环境变量 |
|------|---------------|
| Telegram | `TELEGRAM_BOT_TOKEN` |
| Discord | `DISCORD_BOT_TOKEN` |
| Slack | `SLACK_BOT_TOKEN`, `SLACK_APP_TOKEN` |
| WhatsApp | `WA_ACCESS_TOKEN`, `WA_PHONE_ID`, `WA_VERIFY_TOKEN` |
| Matrix | `MATRIX_TOKEN` |
| Email | `EMAIL_PASSWORD` |

其他通道的环境变量详见上方[全部 45 个通道](#all-44-channels)表格。

---

## 通道覆盖

`ChannelOverrides` 用来让 agent 按通道自定义行为。Sidecar 迁移后**通道级**这一层已经移除（`[channels.<name>.overrides]` 子表挂在已删的进程内 `ChannelsConfig` 上），只剩 `agent.toml` 里的 **agent 级** 覆盖。原来通道级表里的大多数 knob 现在改到对应 sidecar 的 `[sidecar_channels.env]` 环境变量里 —— 详见[频道配置指南里的迁移对照表](/zh/configuration/channels#通道行为覆盖旧版--已移除)。

- **Agent 级** 在 `agent.toml` —— 只对该 agent 在所有通道上生效。加顶层 `[channel_overrides]` 段。

**解析顺序**（迁移后）：agent 级 → sidecar env → 内置默认。哪一层设了哪一层赢。

```toml
# agent.toml —— 每个 agent 自己的覆盖（比如 "这个 agent 永远回 DM"）
[channel_overrides]
dm_policy = "always"
group_policy = "trigger_only"
group_trigger_patterns = ["(?i)\\bmy-bot\\b"]
```

```toml
# config.toml —— 通道级覆盖（历史 —— 已不再被识别）。
# #5463 之前，使用该通道的每个 agent 都继承下面这套默认。保留只为
# 迁移对照参考；新写法把等价值设在 agent 上（`agent.toml [channel_overrides]`）
# 或写到 sidecar 的 `[sidecar_channels.env]` 环境变量（如 `*_DM_POLICY`）。
[channels.discord.overrides]
model = "gemini-2.5-flash"
system_prompt = "You are a concise Discord assistant. Keep replies under 200 words."
dm_policy = "respond"
group_policy = "mention_only"
rate_limit_per_user = 10
threading = true
output_format = "markdown"
usage_footer = "compact"
```

### 覆盖字段

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `model` | `Option<String>` | Agent 默认值 | 覆盖此通道使用的 LLM 模型。 |
| `system_prompt` | `Option<String>` | Agent 默认值 | 覆盖此通道的系统提示词。 |
| `dm_policy` | `DmPolicy` | `Respond` | 如何处理私信。 |
| `group_policy` | `GroupPolicy` | `MentionOnly` | 如何处理群组/频道消息。 |
| `rate_limit_per_user` | `u32` | `0`（无限制） | 每用户每分钟最大消息数。 |
| `threading` | `bool` | `false` | 以线程回复形式发送响应（仅限支持的平台）。 |
| `output_format` | `Option<OutputFormat>` | `Markdown` | 此通道的输出格式。 |
| `usage_footer` | `Option<UsageFooterMode>` | 无 | 是否在响应末尾附加 token 用量信息。 |

---

## 格式化器、速率限制器与策略

### 输出格式化器

`formatter` 模块（`librefang-channels/src/formatter.rs`）将 LLM 输出的 Markdown 转换为平台原生格式：

| OutputFormat | 目标格式 | 说明 |
|-------------|----------|------|
| `Markdown` | 标准 Markdown | 默认值；原样传递。 |
| `TelegramHtml` | Telegram HTML 子集 | 将 `**bold**` 转换为 `<b>`、`` `code` `` 转换为 `<code>` 等。 |
| `SlackMrkdwn` | Slack mrkdwn | 将 `**bold**` 转换为 `*bold*`、链接转换为 `<url\|text>` 等。 |
| `PlainText` | 纯文本 | 去除所有格式。 |

### 按用户速率限制器

`ChannelRateLimiter`（`librefang-channels/src/rate_limiter.rs`）使用 `DashMap` 跟踪每用户消息计数。当通道覆盖中设置了 `rate_limit_per_user` 时，限制器以滑动窗口方式限制每分钟最多 N 条消息。超出限制的消息会收到友好的拒绝提示。

### DM 策略

控制适配器如何处理私信：

| DmPolicy | 行为 |
|----------|------|
| `Respond` | 回复所有私信（默认）。 |
| `AllowedOnly` | 仅回复 `allowed_users` 中用户的私信。 |
| `Ignore` | 静默丢弃所有私信。 |

### 群组策略

控制适配器如何处理群聊、频道和房间中的消息：

| GroupPolicy | 行为 |
|-------------|------|
| `All` | 回复群组中的所有消息。 |
| `MentionOnly` | 仅在 Bot 被 @提及时回复（默认）。 |
| `CommandsOnly` | 仅回复 `/command` 命令消息。 |
| `Ignore` | 静默忽略所有群组消息。 |

策略在 `dispatch_message()` 中执行，早于消息到达 Agent 循环。这意味着被忽略的消息不会消耗任何 LLM token。

---

## Agent 路由

`AgentRouter` 决定哪个 Agent 接收传入的消息。路由逻辑如下：

1. **按通道默认值**：每个通道配置都有一个 `default_agent` 字段。来自该通道的消息会发送到该 Agent。
2. **用户-Agent 绑定**：如果用户之前已与特定 Agent 关联（通过命令或配置），来自该用户的消息会路由到该 Agent。
3. **命令前缀**：用户可以在聊天中发送类似 `/agent coder` 的命令来切换 Agent。后续消息将路由到 "coder" Agent。
4. **兜底**：如果没有匹配的路由规则，消息会发送到第一个可用的 Agent。

当渠道配置了 `default_agent` 时，来自该渠道的消息会跳过语义路由和关键词路由，直接发送到指定的 Agent。用户仍可通过 `/agent` 命令手动切换 Agent。

---

## 出站标记

在 `ChannelOverrides` 里设 `prefix_agent_name` 给每条出站消息加 agent 名前缀。多个 agent 共享一个频道时有用——读者一眼能看出是哪个 agent 回的。

| 风格 | 包装效果 |
|---|---|
| `Off`（默认） | 不变——跟 agent 文本字节相同 |
| `Bracket` | `[agent-name] 回复内容` |
| `BoldBracket` | `**[agent-name]** 回复内容`（粗体渲染依赖通道的输出格式） |

把 `prefix_agent_name` 设到 agent 清单（`agent.toml`）的 `[channel_overrides]` 里；迁移前的 `[channels.<name>.overrides]` 位置已不再被识别。

```toml
# agent.toml
[channel_overrides]
prefix_agent_name = "BoldBracket"
```

包装在每个非流式成功路径上跑一次（`auto_reply`、kernel-streaming-with-status accumulated、streaming-fallback `buffered_text`、非流式 fallback、re-resolution 后重试、`dispatch_with_blocks`）。streaming tee（每个 chunk 到达就转发）**不**包装——前缀会跟 chunk 边界穿插。

## Signal 默认纯文本

Signal 默认 `OutputFormat::PlainText`，原因是 `signal-cli` 会把 Markdown 的 `*` 和 `_` 当字面量渲染，保留会产生可见噪音。这个映射写在 `default_output_format_for_channel("signal")` 中（见 `crates/librefang-channels/src/formatter.rs`），按 `channel_type` 字符串匹配；sidecar 迁移后 `[[sidecar_channels]]` 里仍然保留 `channel_type = "signal"`，所以默认值依旧生效。

`[[sidecar_channels]]` 目前不暴露 `output_format` 的逐通道覆盖（原 `overrides` 块挂在 `[channels.signal]` 之类的进程内配置上，已随之一起删除）。如果某个 agent 必须原样发送 Markdown，请在 agent 这一端翻转 format。

其他通道保留各自的 formatter 默认值——只翻了 Signal。

## 反应表情与处理状态

几个 adapter 可以贴一个反应到用户消息表示"我在处理"，发送回复时移除。各通道独立：

- **Slack** — `reactions_enabled` 开关（环境变量 `SLACK_REACTIONS`，默认 `true`）。收消息加 👀，回复时换成 ✅。`already_reacted` / `no_reaction` 错误静默忽略（fail-open）。
- **Feishu** — 收消息加 `Typing` reaction，回复发送时移除。两个调用都 fire-and-forget；API 失败 `warn!` 但绝不阻塞消息处理。

要在自定义 adapter 上加同样能力，把 reaction 调用 `tokio::spawn` 出去，把 `(reaction_id, message_id)` 存在按 chat id 索引的 map 里。

## Feishu @提及保留

Feishu 之前会静默删掉传入文本里的 `@_user_N` 占位符。Feishu adapter 现在用 mention payload 解析出的 `@<display-name>` 替换（`name` 缺失时回退 `open_id`），并把 `@_all` 重写为 `@all`。agent 看到原始对话语气——"@alice 你能 check 下吗？"——而不是裸标点。

## Signal 媒体附件

已下线的进程内 Rust adapter 支持下载媒体 URL、base64 编码后塞进 `POST /v2/send` 的 `base64_attachments`，覆盖 `Image`、`Voice`、`Video`、`Audio`、`Animation`、`File`、`FileData`、`MediaGroup`。Sidecar 迁移（`librefang.sidecar.adapters.signal`）之后，非文本内容暂时统一退回 `(Unsupported content type)` 占位符；恢复 base64 round-trip 是后续工作。

## WhatsApp DM / 群消息策略

迁移到 sidecar（`librefang.sidecar.adapters.whatsapp`）后，DM / 群组策略改通过 `[[sidecar_channels]]` 块上的环境变量配置：

- `WHATSAPP_DM_POLICY`：`respond`（默认）/ `allowed_only` / `ignore`。`allowed_only` 与 `WHATSAPP_ALLOWED_USERS` 配合使用。
- `WHATSAPP_GROUP_POLICY`：`all`（默认）/ `mention_only` / `commands_only` / `ignore`。`mention_only` 模式下检查文本是否包含 `WHATSAPP_BOT_PHONE`（带/不带 `@`、去 `+`）或 `WHATSAPP_BOT_NAME`，大小写不敏感。

**Cloud API 模式** 出站支持 voice / image / file / location（通过标准 sidecar `Content` 变体）。**Web/QR（Baileys）模式** 把非文本内容降级为文本（voice URL → `(Voice message: <url>)`、无标题 image → `(Image — not supported in Web mode)`），对应原 Rust 适配器 `whatsapp.rs:493-519` 的 gateway-mode fallback。

## Webhook `deliver_only` 模式

Webhook sidecar 可作为 pass-through 跑：传入 payload 完全绕过 LLM（无 sanitizer、无 rate limiter、无 agent 查找），直接转发到目标通道。适合只需要让消息落到 Telegram / Slack 等地方的推送通知和带外告警。

```toml
[[sidecar_channels]]
name = "webhook"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.webhook"]
channel_type = "webhook"
[sidecar_channels.env]
WEBHOOK_LISTEN_PORT = "8461"
WEBHOOK_DELIVER_ONLY = "1"
WEBHOOK_DELIVER = "telegram:123456789"  # WEBHOOK_DELIVER_ONLY=1 时必填
```

`WEBHOOK_SECRET` 写入 `~/.librefang/secrets.env`。`WEBHOOK_DELIVER_ONLY=1` 时若 `WEBHOOK_DELIVER` 为空，sidecar **启动时 fail-closed**——原 Rust validator 同位置只 warn-and-continue，结果运行时入站被静默丢弃。扇出在内部用 `__deliver_only__` / `__deliver_target__` metadata 信号化，kernel `bridge.rs` 路由读这两个字段后短路 LLM 调用。

## 通道文件下载

Telegram（及其他通道）发的文件以**临时认证 URL** 形式过来，**LLM 无法直接访问**。bridge 现在下载到可配置目录，把消息重写成本地路径 content block——非图片文件变成 `ContentBlock::Text` 带保存路径（agent 调 `file_read`），图片变成 `ContentBlock::ImageFile`。

```toml
[channels]
file_download_dir = "/var/lib/librefang/channel-files"   # 默认：~/.librefang/data/channel-files
file_download_max_bytes = 33554432                       # 默认：32 MiB
```

启动 sweep 移除 24h 以上的过期文件；下载过程中还有概率 1/256 的额外 sweep，长跑的守护进程不会堆积孤儿附件。
