22 KiB
Bridge 平台协议规范
版本:1.0-draft
状态:草案 — 实现前可能调整
概述
Bridge 协议允许使用任何编程语言编写的外部平台适配器在运行时通过 WebSocket 动态接入 cc-connect,无需编写 Go 代码或重新编译二进制文件。
架构
┌──────────────────────────────────────────────────────┐
│ cc-connect │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────────┐ │
│ │ Telegram │ │ 飞书 │ │ BridgePlatform │ │
│ │ (原生) │ │ (原生) │ │ (WebSocket) │ │
│ └─────┬──────┘ └─────┬──────┘ └───────┬────────┘ │
│ │ │ │ │
│ └──────────────┴────────────────┘ │
│ │ │
│ ┌─────┴─────┐ │
│ │ Engine │ │
│ └───────────┘ │
└──────────────────────────────────────────────────────┘
│ WebSocket
┌──────────┴───────────┐
│ │
┌──────────┴──────┐ ┌───────────┴─────┐
│ Python 适配器 │ │ Node.js 适配器 │
│ (微信公众号等) │ │ (自定义聊天等) │
└─────────────────┘ └─────────────────┘
BridgePlatform 是 cc-connect 内置的一个平台实现,它:
- 暴露 WebSocket 端点供外部适配器连接。
- 将 WebSocket 消息转换为
core.Platform接口调用。 - 将 Engine 的回复通过同一个 WebSocket 连接推送回适配器。
连接
端点
ws://<host>:<port>/bridge/ws
端口和路径通过 config.toml 配置:
[bridge]
enabled = true
port = 9810
path = "/bridge/ws" # 可选,默认 "/bridge/ws"
token = "your-secret" # 认证密钥,必填
认证
适配器连接时必须通过以下方式之一进行身份验证:
| 方式 | 示例 |
|---|---|
| URL 查询参数 | ws://host:9810/bridge/ws?token=your-secret |
| 请求头 | Authorization: Bearer your-secret |
| 请求头 | X-Bridge-Token: your-secret |
未认证的连接将被拒绝并返回 HTTP 401。
连接生命周期
适配器 cc-connect
│ │
│──── WebSocket 连接 ─────────────→│ (携带 token)
│ │
│──── register ──────────────────→│ (声明平台名和能力)
│←─── register_ack ──────────────│ (确认或拒绝)
│ │
│←──→ message / reply 消息交换 ──→│ (双向)
│ │
│──── ping ──────────────────────→│ (心跳保活,建议 30 秒)
│←─── pong ──────────────────────│
│ │
│──── close ─────────────────────→│ (优雅断开)
消息协议
所有消息均为 JSON 对象,必须包含 type 字段。协议使用 WebSocket 文本帧传输(每帧一个 JSON 对象)。
适配器 → cc-connect
register
连接后必须发送的第一条消息。声明适配器身份和支持的能力。
{
"type": "register",
"platform": "wechat",
"capabilities": ["text", "image", "file", "audio", "card", "buttons", "typing", "update_message", "preview"],
"metadata": {
"version": "1.0.0",
"description": "微信公众号适配器"
}
}
字段说明:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
type |
string | 是 | "register" |
platform |
string | 是 | 唯一平台名称(小写字母、数字、连字符)。用于组成 session key。 |
capabilities |
string[] | 是 | 支持的能力列表(见能力声明)。 |
metadata |
object | 否 | 自由格式的元信息,用于日志/调试。 |
message
将用户消息传递给引擎。
{
"type": "message",
"msg_id": "msg-001",
"session_key": "wechat:user123:user123",
"user_id": "user123",
"user_name": "Alice",
"content": "你好,你能做什么?",
"reply_ctx": "conv-abc-123",
"images": [],
"files": [],
"audio": null
}
字段说明:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
type |
string | 是 | "message" |
msg_id |
string | 是 | 平台消息 ID,用于追踪。 |
session_key |
string | 是 | 唯一会话标识。格式:{platform}:{scope}:{user}。由适配器定义组合方式。 |
user_id |
string | 是 | 用户在平台上的唯一标识。 |
user_name |
string | 否 | 显示名称。 |
content |
string | 是 | 文本内容。 |
reply_ctx |
string | 是 | 不透明的上下文字符串,适配器需要它来路由回复。cc-connect 会在每个回复中原样回传。 |
images |
Image[] | 否 | 附带的图片(见图片对象)。 |
files |
File[] | 否 | 附带的文件(见文件对象)。 |
audio |
Audio | 否 | 语音消息(见音频对象)。 |
card_action
用户点击了卡片上的按钮或选择了选项。
{
"type": "card_action",
"session_key": "wechat:user123:user123",
"action": "cmd:/new",
"reply_ctx": "conv-abc-123"
}
字段说明:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
type |
string | 是 | "card_action" |
session_key |
string | 是 | 触发操作的会话。 |
action |
string | 是 | 按钮的回调值(如 "cmd:/new"、"nav:/model"、"act:/heartbeat pause")。 |
reply_ctx |
string | 是 | 用于路由响应的回复上下文。 |
preview_ack
确认预览消息已创建,返回用于后续更新的 handle。
{
"type": "preview_ack",
"ref_id": "preview-req-001",
"preview_handle": "platform-msg-id-789"
}
ping
心跳保活。cc-connect 回应 pong。
{
"type": "ping",
"ts": 1710000000000
}
cc-connect → 适配器
register_ack
确认或拒绝注册。
{
"type": "register_ack",
"ok": true,
"error": ""
}
reply
发送完整回复消息给用户。
{
"type": "reply",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"content": "我可以帮你完成编码任务!",
"format": "text"
}
字段说明:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
type |
string | 是 | "reply" |
session_key |
string | 是 | 目标会话。 |
reply_ctx |
string | 是 | 来自原始消息的回传。 |
content |
string | 是 | 回复文本内容。 |
format |
string | 否 | "text"(默认)或 "markdown"。 |
reply_stream
流式增量内容,用于实时打字预览。仅在适配器声明了 "preview" 能力时发送。
{
"type": "reply_stream",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"delta": "部分内容...",
"full_text": "累积的完整文本...",
"preview_handle": "platform-msg-id-789",
"done": false
}
| 字段 | 类型 | 说明 |
|---|---|---|
delta |
string | 自上次流式消息以来的新增文本。 |
full_text |
string | 完整累积文本。适配器可用于"替换整条消息"的更新方式。 |
preview_handle |
string | 由 preview_ack 返回的 handle。首条流式消息时为空。 |
done |
bool | 最后一条流式消息时为 true。 |
preview_start
请求适配器创建初始预览消息(用于流式输出)。
{
"type": "preview_start",
"ref_id": "preview-req-001",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"content": "思考中..."
}
适配器应发送消息后回应 preview_ack,包含平台消息 ID。
update_message
请求适配器原地编辑已有消息。用于流式预览更新。
{
"type": "update_message",
"session_key": "wechat:user123:user123",
"preview_handle": "platform-msg-id-789",
"content": "更新后的文本内容..."
}
delete_message
请求适配器删除消息(如清理预览消息)。
{
"type": "delete_message",
"session_key": "wechat:user123:user123",
"preview_handle": "platform-msg-id-789"
}
card
发送结构化卡片给用户。仅在适配器声明了 "card" 能力时发送;否则 cc-connect 会降级为 reply,内容使用 card.RenderText() 生成的纯文本。
{
"type": "card",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"card": {
"header": {
"title": "模型选择",
"color": "blue"
},
"elements": [
{
"type": "markdown",
"content": "请选择一个模型:"
},
{
"type": "actions",
"buttons": [
{"text": "GPT-4", "btn_type": "primary", "value": "cmd:/model switch gpt-4"},
{"text": "Claude", "btn_type": "default", "value": "cmd:/model switch claude"}
],
"layout": "row"
},
{
"type": "divider"
},
{
"type": "note",
"text": "当前模型:gpt-4"
}
]
}
}
完整卡片元素参见卡片 Schema。
buttons
发送带有内联按钮的消息。仅在适配器声明了 "buttons" 能力时发送。
{
"type": "buttons",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"content": "允许执行工具:bash(rm -rf /tmp/old)?",
"buttons": [
[
{"text": "✅ 允许", "data": "perm:req-123:allow"},
{"text": "❌ 拒绝", "data": "perm:req-123:deny"}
]
]
}
buttons 是二维数组:每个内层数组是一行按钮。
typing_start
请求适配器显示"正在输入"指示器。
{
"type": "typing_start",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123"
}
typing_stop
请求适配器隐藏"正在输入"指示器。
{
"type": "typing_stop",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123"
}
audio
发送语音/音频消息。仅在适配器声明了 "audio" 能力时发送。
{
"type": "audio",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"data": "<base64 编码的音频数据>",
"format": "mp3"
}
image
发送图片给用户。仅在适配器声明了 "image" 能力时发送。
{
"type": "image",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"data": "<base64 编码的图片数据>",
"mime_type": "image/png",
"file_name": "screenshot.png"
}
file
发送文件给用户。仅在适配器声明了 "file" 能力时发送。
{
"type": "file",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"data": "<base64 编码的文件数据>",
"mime_type": "application/pdf",
"file_name": "report.pdf"
}
pong
对 ping 的回应。
{
"type": "pong",
"ts": 1710000000000
}
error
通知适配器服务端错误。
{
"type": "error",
"code": "session_not_found",
"message": "找不到给定 key 的活跃会话"
}
数据 Schema
能力声明
| 能力 | 说明 | 启用的消息类型 |
|---|---|---|
text |
基础文本消息(必须) | message、reply |
image |
收发图片 | message.images、image 回复 |
file |
收发文件 | message.files、file 回复 |
audio |
收发语音消息 | message.audio、audio 回复 |
card |
结构化富卡片渲染 | card 回复 |
buttons |
可点击的内联按钮 | buttons 回复、card_action |
typing |
正在输入指示器 | typing_start、typing_stop |
update_message |
编辑已有消息 | update_message |
preview |
流式预览(需要 update_message) |
preview_start、reply_stream |
delete_message |
删除消息 | delete_message |
reconstruct_reply |
可从 session_key 重建回复上下文 | 启用定时任务/心跳消息 |
如果未声明某个能力,cc-connect 会自动降级:
- 没有
card→ 卡片通过RenderText()渲染为纯文本。 - 没有
buttons→ 按钮被省略或渲染为文本提示。 - 没有
preview→ 禁用流式预览;只发送最终回复。 - 没有
typing→ 跳过输入指示器。
图片对象
{
"mime_type": "image/png",
"data": "<base64 编码>",
"file_name": "screenshot.png"
}
文件对象
{
"mime_type": "application/pdf",
"data": "<base64 编码>",
"file_name": "report.pdf"
}
音频对象
{
"mime_type": "audio/ogg",
"data": "<base64 编码>",
"format": "ogg",
"duration": 5
}
卡片 Schema
卡片由可选的 header 和元素列表组成:
{
"header": {
"title": "卡片标题",
"color": "blue"
},
"elements": [ ... ]
}
支持的颜色: blue、green、red、orange、purple、grey、turquoise、violet、indigo、wathet、yellow、carmine。
元素类型
Markdown 文本
{"type": "markdown", "content": "**加粗** 和 _斜体_"}
分割线
{"type": "divider"}
操作按钮行
{
"type": "actions",
"buttons": [
{"text": "点我", "btn_type": "primary", "value": "cmd:/do-something"}
],
"layout": "row"
}
btn_type:"primary"、"default"、"danger"。
layout:"row"(默认)、"equal_columns"。
列表项(描述 + 按钮)
{
"type": "list_item",
"text": "GPT-4 — 最强模型",
"btn_text": "选择",
"btn_type": "primary",
"btn_value": "cmd:/model switch gpt-4"
}
下拉选择器
{
"type": "select",
"placeholder": "选择一个模型",
"options": [
{"text": "GPT-4", "value": "cmd:/model switch gpt-4"},
{"text": "Claude", "value": "cmd:/model switch claude"}
],
"init_value": "cmd:/model switch gpt-4"
}
脚注
{
"type": "note",
"text": "提示:使用 /help 查看所有命令",
"tag": "可选的机器标签"
}
Session Key 格式
Session key 遵循以下格式:
{platform}:{scope}:{user_id}
- platform:注册时的
platform名称(如wechat)。 - scope:分组范围 — 可以是群/频道 ID,也可以与
user_id相同(一对一私聊)。 - user_id:用户在平台上的唯一标识。
示例:
wechat:user123:user123— 私聊wechat:group456:user123— 用户在群聊中matrix:room789:alice— Matrix 聊天室
适配器负责构建一致的 session key。
会话管理 REST API
除了用于实时消息的 WebSocket 协议外,Bridge Server 还在同一端口上暴露 HTTP REST 端点用于会话管理。适配器可以通过这些接口列出、创建、切换和删除会话,无需单独配置管理 API。
认证
使用与 WebSocket 连接相同的 token:
| 方式 | 示例 |
|---|---|
| Header | Authorization: Bearer your-secret |
| Query 参数 | ?token=your-secret |
响应格式
所有响应使用统一的信封格式:
{"ok": true, "data": { ... }}
{"ok": false, "error": "错误信息"}
端点
所有端点相对于 Bridge Server 基础 URL(如 http://localhost:9810)。
GET /bridge/sessions
列出指定 session key 的所有会话。
Query 参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
session_key |
string | 是 | 要查询会话的 session key(如 wechat:user123:user123)。 |
响应:
{
"ok": true,
"data": {
"sessions": [
{
"id": "s1",
"name": "default",
"history_count": 12
},
{
"id": "s2",
"name": "work",
"history_count": 5
}
],
"active_session_id": "s1"
}
}
POST /bridge/sessions
创建新的命名会话。
请求体:
{
"session_key": "wechat:user123:user123",
"name": "work"
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
session_key |
string | 是 | 用户的 session key。 |
name |
string | 否 | 人类可读的会话名称。默认为 "default"。 |
响应:
{
"ok": true,
"data": {
"id": "s3",
"name": "work",
"message": "session created"
}
}
GET /bridge/sessions/{id}
获取会话详情及消息历史。
Query 参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
session_key |
string | (必填) | 用于定位项目上下文的 session key。 |
history_limit |
int | 50 | 返回的最大历史条数。 |
响应:
{
"ok": true,
"data": {
"id": "s1",
"name": "default",
"history": [
{"role": "user", "content": "你好"},
{"role": "assistant", "content": "你好!有什么可以帮你的?"}
]
}
}
DELETE /bridge/sessions/{id}
删除会话及其历史记录。
Query 参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
session_key |
string | 是 | 用于定位项目上下文的 session key。 |
响应:
{
"ok": true,
"data": {
"message": "session deleted"
}
}
POST /bridge/sessions/switch
切换指定 session key 的活跃会话。
请求体:
{
"session_key": "wechat:user123:user123",
"target": "s2"
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
session_key |
string | 是 | Session key。 |
target |
string | 是 | 要切换到的会话 ID 或名称。 |
响应:
{
"ok": true,
"data": {
"message": "session switched",
"active_session_id": "s2"
}
}
错误处理
断线重连
WebSocket 连接断开时,适配器应:
- 使用指数退避等待(起始 1 秒,最大 60 秒)。
- 重新连接并发送新的
register消息。 - 恢复正常运行 — cc-connect 独立于连接维护会话状态。
消息顺序
单个 WebSocket 连接内的消息是有序的。cc-connect 按 session key 顺序处理适配器消息。
超时
- Ping 间隔:适配器应至少每 30 秒发送一次
ping。 - 连接超时:cc-connect 在 90 秒没有收到 ping 后关闭空闲连接。
- 回复超时:如果 agent 耗时过长,cc-connect 可能发送错误回复。适配器不需要特殊处理。
配置示例
[bridge]
enabled = true
port = 9810
token = "一个强随机密钥"
# 可选:限制哪些适配器可以连接(按平台名称)。
# 默认:允许所有已注册的适配器。
# allow_platforms = ["wechat", "matrix"]
不需要为每个适配器单独配置项目 — 适配器默认关联到默认项目,或在 register 消息中指定 project 字段绑定到特定项目。
SDK 开发指南
开发适配器时,请遵循以下原则:
- 保持无状态 — 适配器应该是一个轻量的协议转换层。所有会话状态存储在 cc-connect 中。
- 处理断线重连 — 网络故障是正常的,实现指数退避重试。
- 如实声明能力 — 只声明你的平台实际支持的能力。
- 忠实使用
reply_ctx— 始终原样回传原始消息中的reply_ctx。 - 二进制数据用 Base64 — 图片、文件和音频通过 base64 编码字符串传输。
- 记录错误而非崩溃 — 收到未知消息类型时,记录日志并继续运行。
最小适配器示例(Python 伪代码)
import asyncio
import json
import websockets
async def main():
uri = "ws://localhost:9810/bridge/ws?token=your-secret"
async with websockets.connect(uri) as ws:
# 1. 注册
await ws.send(json.dumps({
"type": "register",
"platform": "my-chat",
"capabilities": ["text", "buttons"]
}))
ack = json.loads(await ws.recv())
assert ack["ok"], f"注册失败: {ack['error']}"
# 2. 启动消息循环
async def recv_loop():
async for raw in ws:
msg = json.loads(raw)
if msg["type"] == "reply":
send_to_chat_platform(msg["reply_ctx"], msg["content"])
elif msg["type"] == "buttons":
send_buttons_to_chat(msg["reply_ctx"], msg["content"], msg["buttons"])
# ... 处理其他类型
async def send_loop():
while True:
chat_msg = await get_next_chat_message()
await ws.send(json.dumps({
"type": "message",
"msg_id": chat_msg.id,
"session_key": f"my-chat:{chat_msg.user_id}:{chat_msg.user_id}",
"user_id": chat_msg.user_id,
"user_name": chat_msg.user_name,
"content": chat_msg.text,
"reply_ctx": chat_msg.conversation_id
}))
await asyncio.gather(recv_loop(), send_loop())
asyncio.run(main())
版本管理
协议版本通过 register 消息的 metadata.protocol_version 声明。当前版本为 1。cc-connect 会拒绝不兼容版本的连接,并在 register_ack 中返回错误。
{
"type": "register",
"platform": "my-chat",
"capabilities": ["text"],
"metadata": {
"protocol_version": 1
}
}