消息渠道与适配器

适用版本:ChengOS v0.1.0+ | 最后核对:2026-08-12 | 来源:crates/cheng-adapters/srccrates/cheng-core/src/ports/channel_adapter.rscrates/cheng-core/src/domain/channelcrates/cheng-api/src/services/adapter_health.rscrates/cheng-api/src/services/channel_ingest.rs

渠道把一个外部即时通讯平台接到 ChengOS 的工作流上。消息从 Telegram 或 Slack 到达,平台特有的载荷被归一化成同一个消息模型,会话被解析出来,工作流随即运行。回复再沿同一个适配器发回去。

开箱自带五个第一方适配器:TelegramWhatsAppSlackWeCom(企业微信)和 DingTalk(钉钉)。

各司其职

框架与适配器之间的分工是严格的,知道这条分界线能省下你去错地方找问题的时间:

框架负责 适配器负责
chk_ API 密钥校验 把平台载荷翻译成归一化消息
会话解析 把归一化命令翻译成平台 API 调用
消息持久化 平台连接建立(令牌、OAuth、二维码)
工作流执行 校验 Webhook 真实性(签名/令牌)
WebSocket 广播 应答平台的验证挑战
计算 Webhook URL

适配器查找始终以原始的 app_type 字符串为键,而不是枚举。这是刻意的:它给用其他语言写的外部适配器留出了空间,让它们能以任意 app_type 值注册而无需改动核心。未知的 app_type 返回「未找到」,而不是报错。

各适配器

适配器 认证模式 传输方式
Telegram webhook_token 长轮询(默认)或 Webhook
WhatsApp 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_messagegroup_chatreactionsmessage_editmedia_uploadtyping_indicatorwebhookslong_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_tokenxapp-…)只在 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_textsend_mediareplyreacttyping。在平台支持的情况下,idempotency_key 可防止重试造成重复发送。

错误

适配器故障被归入若干稳定的类别——ConfigAuthPlatformApiNormalizationNotSupportedInternal。这些含义是契约性的:可以新增变体,但既有变体绝不会被改作他用。渠道出问题时,类别会告诉你该去看配置、看凭证,还是看平台。

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 令牌。

下一步

© 版权声明
THE END
喜欢就支持一下吧
点赞14 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容