适用版本:ChengOS v0.1.0+ | 最后核对:2026-08-12 | 来源:
chengflow-sdk/README.md、chengflow-sdk/src/sdk/session-manager.ts、crates/cheng-core/src/domain/channel/mod.rs、crates/cheng-core/src/ports/conversation_repo.rs、crates/cheng-api/src/services/conversation_command.rs
一条入站消息必须回答两个彼此独立的问题:它属于哪一个会话? 以及 该运行哪一个工作流? 把二者混为一谈是网关建模中最常见的错误,ChengOS 刻意让它们分开。
三种身份
| 身份 | 回答什么 | 在哪里决定 |
|---|---|---|
channelId |
消息来自哪个应用/哪条连接 | 渠道记录 |
workflowId |
该运行什么 | 在渠道配置阶段绑定 |
session / external_chat_id |
这属于哪个会话 | 每个外部聊天各一个 |
channelId 不是工作流标识。 它说明是哪个应用发来的消息、这个会话归哪条连接所有,网关用它把结果路由回正确的应用。工作流由一次显式绑定选定——渠道记录上的 bound_workflow_id——绝不从 channelId 推断。
这条分离是踩过坑才学到的:在网关重构之前,channelId 被直接映射成工作流,而浏览器前端还复用旧式适配器密钥去调 /channel/*。三件事——应用来源标识、工作流选择、适配器级鉴权——纠缠在同一个值上。现在它们被分开建模。
渠道记录
Channel
├── channel_id ← 外部标识,工作区内唯一,
│ 同时是 Webhook 的 URL 路径段
├── app_type ← 路由键:原始字符串,绝不是枚举
├── bound_workflow_id ← 消息到达时运行什么
├── connection_config ← 对框架不透明
├── connection_state
├── setup_data
└── enabled
app_type 选适配器,bound_workflow_id 选工作流。两者都是显式字段,谁也不从对方猜出来。
会话解析
入站消息通过一个三元组映射到会话:
(workspace_id, app_id, external_chat_id) ──► Conversation
find_or_create_by_channel 是幂等的:用同一个三元组反复调用会返回同一个会话。正因如此,一次 Webhook 重试——或者平台重复投递一条消息——才不会造成伤害。
会话还存了 external_user_id,因此一个群聊是一个带发送者归属的会话,而不是每个参与者一个会话。
轮换:从头开始
~new 或切换工作流,绝不能留下两个会话都声称拥有同一个外部聊天。因此轮换在一个事务里做两件事:
- 给当前会话的
external_chat_id加上__retired_<ts>后缀,使其退役、不再匹配后续查找。 - 创建一个拥有规范映射、并绑定到新工作流的新会话。
任何时刻都不存在两个活跃会话拥有同一外部映射的窗口。测试用的内存实现顺序执行这两步;最坏的交错也只是下一条消息时重新创建会话,而不会产生重复映射。
来自外部渠道的命令
Telegram 或企业微信上的用户没有工具条,所以路由控制以文本命令的形式暴露:
~new 新建会话
~workflow 列出工作流(回复数字切换)
~workflow <名称> [消息] 切换工作流,可同时发一条消息
~channel 列出渠道
~<渠道名> [消息] 切换渠道
~status 显示当前工作区/工作流
~cancel 取消正在运行的执行或未完成的选择
~~shortcuts 列出路由快捷指令(回复数字切换)
~~presets 列出节点预设(回复数字应用)
~~y 批准一个待决请求
~~n [原因] 拒绝一个待决请求
~help 查看帮助
有两处细节值得知道:
- 数字回复是有状态的。
~workflow列出选项并把这份选择存下来;回复3就在这份存下来的列表里解析。显式的~workflow <n>也只针对已存的选择解析,因此没有先列表就直接发数字不会有任何效果。 ~<渠道名>按前缀匹配。 解析器先试整段文本,再试逐步变短的、以空白分隔的前缀,并同时匹配channel_id和name——正因如此,~support 我的订单到哪了才能一行之内既切渠道又发消息。- 审批是无条件的。
~~y批准、~~n [原因]拒绝;有条件审批被明确不支持,会带着提示被拒绝,而不是半执行。
这些命令住在会话命令服务里,而不在摄取管线里,因此外部渠道、CLI 和测试共享同一份实现。
网关 SDK 模型
chengflow-app 网关(代码在 chengflow-sdk/)是一个前端网关层,不是第二个编辑器。它的职责是:
外部应用
↓
网关完成配对与连接
↓
网关按 channelId 找到应用映射,按 workflowId 决定执行目标
↓
网关用用户的 JWT 调用后端
↓
后端执行工作流
↓
后端返回结果/推送执行事件
↓
网关按 channelId + 会话映射把结果转发回来源应用
浏览器网关只用 JWT 鉴权。 它不使用 chk_ 适配器密钥——那属于服务端的平台服务。这是一条硬边界:浏览器前端一旦持有适配器级密钥,就等于把一个能代表整条渠道行事的凭证发了出去。
浏览器里的会话
SDK 的会话管理器持有映射的客户端那一半:
- 为每个
channelId生成一个sessionId(UUID v4)并持久化到localStorage,因此同一浏览器中的同一应用在刷新后总能恢复同一个会话。 sessionId映射到后端的external_chat_id,它与(workspace_id, app_id)一起唯一确定一个会话。- 重置会话会铸造新的
sessionId并清除关联的会话 id——相当于浏览器里的~new。 - 支持同一渠道下多个会话,因此一个应用可以承载多路并行对话。
选择路由形态
| 你想要 | 这样做 |
|---|---|
| 一个应用,一种行为 | 在渠道上绑定工作流,完事 |
| 一个应用,多种行为 | 绑定一个按消息内容分支的路由工作流 |
| 多个应用,一种行为 | 建多个渠道,全部绑定同一个工作流 |
| 逐回合的路由控制 | 快捷指令(~~shortcuts,或编辑器工具条) |
第四行是最常被忽略的:快捷指令能给单个回合附加结构化路由数据,而不改动渠道的绑定。
下一步
- 即时通讯渠道——为这一切供料的适配器。
- 网关模型:应用、渠道、会话——SDK 视角的深入版本。
- 渠道问题排查——消息到了却什么都没跑起来时。

暂无评论内容