适用版本:ChengOS v0.1.0+ | 最后核对:2026-08-12 | 来源:
crates/cheng-adapters/src、crates/cheng-core/src/ports/channel_adapter.rs、crates/cheng-core/src/domain/channel、crates/cheng-api/src/services/adapter_health.rs、crates/cheng-api/src/services/channel_ingest.rs
渠道把一个外部即时通讯平台接到 ChengOS 的工作流上。消息从 Telegram 或 Slack 到达,平台特有的载荷被归一化成同一个消息模型,会话被解析出来,工作流随即运行。回复再沿同一个适配器发回去。
开箱自带五个第一方适配器:Telegram、WhatsApp、Slack、WeCom(企业微信)和 DingTalk(钉钉)。
各司其职
框架与适配器之间的分工是严格的,知道这条分界线能省下你去错地方找问题的时间:
| 框架负责 | 适配器负责 |
|---|---|
chk_ API 密钥校验 |
把平台载荷翻译成归一化消息 |
| 会话解析 | 把归一化命令翻译成平台 API 调用 |
| 消息持久化 | 平台连接建立(令牌、OAuth、二维码) |
| 工作流执行 | 校验 Webhook 真实性(签名/令牌) |
| WebSocket 广播 | 应答平台的验证挑战 |
| 计算 Webhook URL |
适配器查找始终以原始的 app_type 字符串为键,而不是枚举。这是刻意的:它给用其他语言写的外部适配器留出了空间,让它们能以任意 app_type 值注册而无需改动核心。未知的 app_type 返回「未找到」,而不是报错。
各适配器
| 适配器 | 认证模式 | 传输方式 |
|---|---|---|
| Telegram | webhook_token |
长轮询(默认)或 Webhook |
webhook_signature |
Webhook,X-Hub-Signature-256 上的 HMAC-SHA256 |
|
| Slack | oauth |
Webhook(默认)或 Socket Mode |
| WeCom | webhook_encrypted_signature |
Webhook,SHA1 签名 + AES-CBC 加密 XML |
| DingTalk | stream_connection |
到钉钉 Stream 网关的长连接 WebSocket |
Telegram、Slack Socket Mode 和 DingTalk 无需公网 HTTPS 地址即可工作。 这是本页最有用的一条事实:如果你把 ChengOS 跑在笔记本或内网里,选 Telegram 轮询、Slack Socket Mode 或钉钉,就完全不必对外暴露任何东西。WhatsApp 和企业微信则需要公网可达的 Webhook。
每个适配器都会声明自己的能力(direct_message、group_chat、reactions、message_edit、media_upload、typing_indicator、webhooks、long_polling……),前端据此按渠道显示或隐藏功能。Telegram 支持「正在输入」但不支持表情回应和消息编辑;Slack 支持回应、编辑和删除,但没有「正在输入」。
配置
配置放在渠道记录的 connection_config JSON 里,框架把它当作不透明数据。各自的形状:
// Telegram
{ "bot_token": "…", "connection_mode": "polling",
"webhook_secret": "only-for-webhook-mode",
"allowed_updates": ["message", "callback_query"] }
// Slack
{ "connection_mode": "webhook", "bot_token": "xoxb-…",
"signing_secret": "…", "app_token": "xapp-…" }
// WhatsApp
{ "phone_number_id": "…", "business_account_id": "…", "access_token": "…",
"signing_secret": "…", "webhook_verify_token": "…", "api_version": "v21.0" }
// WeCom(企业微信)
{ "corp_id": "ww…", "agent_id": "1000002", "corp_secret": "…",
"token": "…", "encoding_aes_key": "<43 位 base64url>" }
// DingTalk(钉钉)
{ "client_id": "…", "client_secret": "…", "robot_code": "…" }
connection_mode 在 Telegram 默认是 polling,在 Slack 默认是 webhook。Slack 的 app_token(xapp-…)只在 Socket Mode 下需要;signing_secret 只在 Webhook 模式下需要。
Webhook URL 是运行时计算出来的,由 PUBLIC_BASE_URL 加渠道 UUID 拼成——你永远不需要存它。Telegram 的 Webhook 模式还会校验该 URL:必须是 HTTPS 且不能是 localhost,若 PUBLIC_BASE_URL 配错,连接会带着明确的提示失败。
连接生命周期
unconfigured ──connect()──► configuring ──complete_connect()──► active
│ │
└────────────(单步完成)─────────────────────┘
│
健康检查失败 / 令牌过期 ──────────────► degraded
│
自动重连成功 ─────────────┘
disconnected(手动断开) error { message }(需人工介入)
基于令牌的适配器在 connect() 内就抵达 active,其 complete_connect() 返回 NotSupported。OAuth 与二维码类适配器返回 configuring,并把跳转 URL 或二维码数据放在 setup_data 里,最后在 complete_connect() 收尾。
connect() 必须是幂等的:超时或部署重启后重试,绝不能产生重复的 Webhook 注册,也不能弄坏远端状态。
健康监控
一个后台服务每 ADAPTER_HEALTH_INTERVAL_SECS(默认 300 秒)检查一次处于 active 和 degraded 的渠道。不健康的 active 渠道被标记为 degraded;degraded 渠道会自动重连并恢复为 active。
依赖后台工作者的平台——Telegram 轮询、Slack Socket Mode、钉钉 Stream——被特殊对待,而其中的道理值得一读:
- 存活性看工作者,而不是看凭证。 令牌有效不等于消息在进来,如果轮询工作者已经退出的话。进程内管理器的
is_running()才是权威信号,工作者一死,渠道立刻被标记为degraded。 - 恢复必须重启工作者。
adapter.connect()只校验凭证;只调它会造成一种假恢复:数据库显示active,而实际上什么都收不到。恢复流程会调用管理器的restart()。
入站:归一化消息
每一份平台载荷都会变成 NormalizedInboundMessage:
external_message_id ← 用于去重
external_user_id
external_chat_id ← 路由键
message_type ← text | image | file | audio | video | location | reaction | event
text、attachments、reply_to_message_id
metadata ← sender_display_name、is_group、thread_id、mentioned_bot……
timestamp
raw ← 原始载荷,留作审计;核心从不解析它
一次 Webhook 投递可能包含多条消息(平台会打包),所以归一化返回的是一个数组;只包含状态更新的投递则返回空数组。
非文本消息会得到一个内容占位符——[image]、[file]、、、[location]、[reaction]、[event:…]——让会话历史保持可读,而真正的媒体会被映射成附件并存为产物。
摄取管线是共享的:Webhook 投递与轮询/Socket 工作者走的是同一段代码,并且它只负责传输层的事。命令语义(~new、~workflow、~~y、数字选择、取消)住在会话命令服务里,因此外部渠道、CLI 和测试共享同一套行为。
出站
回复是 NormalizedOutboundCommand,指明目标会话以及以下之一:send_text、send_media、reply、react、typing。在平台支持的情况下,idempotency_key 可防止重试造成重复发送。
错误
适配器故障被归入若干稳定的类别——Config、Auth、PlatformApi、Normalization、NotSupported、Internal。这些含义是契约性的:可以新增变体,但既有变体绝不会被改作他用。渠道出问题时,类别会告诉你该去看配置、看凭证,还是看平台。
REST 接口
GET|POST /workspaces/:ws/channels
GET|PUT|DELETE /workspaces/:ws/channels/:id
POST|DELETE /workspaces/:ws/channels/:id/connect
POST /workspaces/:ws/channels/:id/connect/complete
GET /workspaces/:ws/channels/:id/status
GET /workspaces/:ws/channels/:id/capabilities
GET /workspaces/:ws/channels/:id/auth-pattern
GET|POST /webhooks/:channel_uuid (无需认证——平台来调)
渠道管理挂在工作区下,受 JWT 保护。Webhook 接口出于必然是不带认证的——真实性由适配器的签名校验建立,而不是靠 Bearer 令牌。

暂无评论内容