Files
cc-connect/docs/bridge-protocol.zh-CN.md
2026-06-02 23:14:41 +08:00

907 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Bridge 平台协议规范
> 版本:1.0-draft
> 状态:草案 — 实现前可能调整
## 概述
Bridge 协议允许使用**任何编程语言**编写的外部平台适配器在运行时通过 WebSocket 动态接入 cc-connect,无需编写 Go 代码或重新编译二进制文件。
### 架构
```
┌──────────────────────────────────────────────────────┐
│ cc-connect │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────────┐ │
│ │ Telegram │ │ 飞书 │ │ BridgePlatform │ │
│ │ (原生) │ │ (原生) │ │ (WebSocket) │ │
│ └─────┬──────┘ └─────┬──────┘ └───────┬────────┘ │
│ │ │ │ │
│ └──────────────┴────────────────┘ │
│ │ │
│ ┌─────┴─────┐ │
│ │ Engine │ │
│ └───────────┘ │
└──────────────────────────────────────────────────────┘
│ WebSocket
┌──────────┴───────────┐
│ │
┌──────────┴──────┐ ┌───────────┴─────┐
│ Python 适配器 │ │ Node.js 适配器 │
│ (微信公众号等) │ │ (自定义聊天等) │
└─────────────────┘ └─────────────────┘
```
`BridgePlatform` 是 cc-connect 内置的一个平台实现,它:
1. 暴露 WebSocket 端点供外部适配器连接。
2. 将 WebSocket 消息转换为 `core.Platform` 接口调用。
3. 将 Engine 的回复通过同一个 WebSocket 连接推送回适配器。
---
## 连接
### 端点
```
ws://<host>:<port>/bridge/ws
```
端口和路径通过 `config.toml` 配置:
```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`
连接后必须发送的第一条消息。声明适配器身份和支持的能力。
```json
{
"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`
将用户消息传递给引擎。
```json
{
"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`
用户点击了卡片上的按钮或选择了选项。
```json
{
"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。
```json
{
"type": "preview_ack",
"ref_id": "preview-req-001",
"preview_handle": "platform-msg-id-789"
}
```
#### `ping`
心跳保活。cc-connect 回应 `pong`
```json
{
"type": "ping",
"ts": 1710000000000
}
```
---
### cc-connect → 适配器
#### `register_ack`
确认或拒绝注册。
```json
{
"type": "register_ack",
"ok": true,
"error": ""
}
```
#### `reply`
发送完整回复消息给用户。
```json
{
"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"` 能力时发送。
```json
{
"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`
请求适配器创建初始预览消息(用于流式输出)。
```json
{
"type": "preview_start",
"ref_id": "preview-req-001",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"content": "思考中..."
}
```
适配器应发送消息后回应 `preview_ack`,包含平台消息 ID。
#### `update_message`
请求适配器原地编辑已有消息。用于流式预览更新。
```json
{
"type": "update_message",
"session_key": "wechat:user123:user123",
"preview_handle": "platform-msg-id-789",
"content": "更新后的文本内容..."
}
```
#### `delete_message`
请求适配器删除消息(如清理预览消息)。
```json
{
"type": "delete_message",
"session_key": "wechat:user123:user123",
"preview_handle": "platform-msg-id-789"
}
```
#### `card`
发送结构化卡片给用户。仅在适配器声明了 `"card"` 能力时发送;否则 cc-connect 会降级为 `reply`,内容使用 `card.RenderText()` 生成的纯文本。
```json
{
"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](#卡片-schema)。
#### `buttons`
发送带有内联按钮的消息。仅在适配器声明了 `"buttons"` 能力时发送。
```json
{
"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`
请求适配器显示"正在输入"指示器。
```json
{
"type": "typing_start",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123"
}
```
#### `typing_stop`
请求适配器隐藏"正在输入"指示器。
```json
{
"type": "typing_stop",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123"
}
```
#### `audio`
发送语音/音频消息。仅在适配器声明了 `"audio"` 能力时发送。
```json
{
"type": "audio",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"data": "<base64 编码的音频数据>",
"format": "mp3"
}
```
#### `image`
发送图片给用户。仅在适配器声明了 `"image"` 能力时发送。
```json
{
"type": "image",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"data": "<base64 编码的图片数据>",
"mime_type": "image/png",
"file_name": "screenshot.png"
}
```
#### `file`
发送文件给用户。仅在适配器声明了 `"file"` 能力时发送。
```json
{
"type": "file",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"data": "<base64 编码的文件数据>",
"mime_type": "application/pdf",
"file_name": "report.pdf"
}
```
#### `pong`
`ping` 的回应。
```json
{
"type": "pong",
"ts": 1710000000000
}
```
#### `error`
通知适配器服务端错误。
```json
{
"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` → 跳过输入指示器。
### 图片对象
```json
{
"mime_type": "image/png",
"data": "<base64 编码>",
"file_name": "screenshot.png"
}
```
### 文件对象
```json
{
"mime_type": "application/pdf",
"data": "<base64 编码>",
"file_name": "report.pdf"
}
```
### 音频对象
```json
{
"mime_type": "audio/ogg",
"data": "<base64 编码>",
"format": "ogg",
"duration": 5
}
```
### 卡片 Schema
卡片由可选的 header 和元素列表组成:
```json
{
"header": {
"title": "卡片标题",
"color": "blue"
},
"elements": [ ... ]
}
```
**支持的颜色:** `blue``green``red``orange``purple``grey``turquoise``violet``indigo``wathet``yellow``carmine`
#### 元素类型
**Markdown 文本**
```json
{"type": "markdown", "content": "**加粗** 和 _斜体_"}
```
**分割线**
```json
{"type": "divider"}
```
**操作按钮行**
```json
{
"type": "actions",
"buttons": [
{"text": "点我", "btn_type": "primary", "value": "cmd:/do-something"}
],
"layout": "row"
}
```
`btn_type``"primary"``"default"``"danger"`
`layout``"row"`(默认)、`"equal_columns"`
**列表项(描述 + 按钮)**
```json
{
"type": "list_item",
"text": "GPT-4 — 最强模型",
"btn_text": "选择",
"btn_type": "primary",
"btn_value": "cmd:/model switch gpt-4"
}
```
**下拉选择器**
```json
{
"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"
}
```
**脚注**
```json
{
"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` |
### 响应格式
所有响应使用统一的信封格式:
```json
{"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`)。 |
**响应:**
```json
{
"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
创建新的命名会话。
**请求体:**
```json
{
"session_key": "wechat:user123:user123",
"name": "work"
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `session_key` | string | 是 | 用户的 session key。 |
| `name` | string | 否 | 人类可读的会话名称。默认为 `"default"`。 |
**响应:**
```json
{
"ok": true,
"data": {
"id": "s3",
"name": "work",
"message": "session created"
}
}
```
---
#### GET /bridge/sessions/{id}
获取会话详情及消息历史。
**Query 参数:**
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `session_key` | string | (必填) | 用于定位项目上下文的 session key。 |
| `history_limit` | int | 50 | 返回的最大历史条数。 |
**响应:**
```json
{
"ok": true,
"data": {
"id": "s1",
"name": "default",
"history": [
{"role": "user", "content": "你好"},
{"role": "assistant", "content": "你好!有什么可以帮你的?"}
]
}
}
```
---
#### DELETE /bridge/sessions/{id}
删除会话及其历史记录。
**Query 参数:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `session_key` | string | 是 | 用于定位项目上下文的 session key。 |
**响应:**
```json
{
"ok": true,
"data": {
"message": "session deleted"
}
}
```
---
#### POST /bridge/sessions/switch
切换指定 session key 的活跃会话。
**请求体:**
```json
{
"session_key": "wechat:user123:user123",
"target": "s2"
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `session_key` | string | 是 | Session key。 |
| `target` | string | 是 | 要切换到的会话 ID 或名称。 |
**响应:**
```json
{
"ok": true,
"data": {
"message": "session switched",
"active_session_id": "s2"
}
}
```
---
## 错误处理
### 断线重连
WebSocket 连接断开时,适配器应:
1. 使用指数退避等待(起始 1 秒,最大 60 秒)。
2. 重新连接并发送新的 `register` 消息。
3. 恢复正常运行 — cc-connect 独立于连接维护会话状态。
### 消息顺序
单个 WebSocket 连接内的消息是有序的。cc-connect 按 session key 顺序处理适配器消息。
### 超时
- **Ping 间隔**:适配器应至少每 30 秒发送一次 `ping`
- **连接超时**cc-connect 在 90 秒没有收到 ping 后关闭空闲连接。
- **回复超时**:如果 agent 耗时过长,cc-connect 可能发送错误回复。适配器不需要特殊处理。
---
## 配置示例
```toml
[bridge]
enabled = true
port = 9810
token = "一个强随机密钥"
# 可选:限制哪些适配器可以连接(按平台名称)。
# 默认:允许所有已注册的适配器。
# allow_platforms = ["wechat", "matrix"]
```
不需要为每个适配器单独配置项目 — 适配器默认关联到**默认项目**,或在 `register` 消息中指定 `project` 字段绑定到特定项目。
---
## SDK 开发指南
开发适配器时,请遵循以下原则:
1. **保持无状态** — 适配器应该是一个轻量的协议转换层。所有会话状态存储在 cc-connect 中。
2. **处理断线重连** — 网络故障是正常的,实现指数退避重试。
3. **如实声明能力** — 只声明你的平台实际支持的能力。
4. **忠实使用 `reply_ctx`** — 始终原样回传原始消息中的 `reply_ctx`
5. **二进制数据用 Base64** — 图片、文件和音频通过 base64 编码字符串传输。
6. **记录错误而非崩溃** — 收到未知消息类型时,记录日志并继续运行。
### 最小适配器示例(Python 伪代码)
```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` 中返回错误。
```json
{
"type": "register",
"platform": "my-chat",
"capabilities": ["text"],
"metadata": {
"protocol_version": 1
}
}
```