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

22 KiB
Raw Permalink Blame History

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 配置:

[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 基础文本消息(必须) messagereply
image 收发图片 message.imagesimage 回复
file 收发文件 message.filesfile 回复
audio 收发语音消息 message.audioaudio 回复
card 结构化富卡片渲染 card 回复
buttons 可点击的内联按钮 buttons 回复、card_action
typing 正在输入指示器 typing_starttyping_stop
update_message 编辑已有消息 update_message
preview 流式预览(需要 update_message preview_startreply_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": [ ... ]
}

支持的颜色: bluegreenredorangepurplegreyturquoisevioletindigowathetyellowcarmine

元素类型

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. 使用指数退避等待(起始 1 秒,最大 60 秒)。
  2. 重新连接并发送新的 register 消息。
  3. 恢复正常运行 — 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 开发指南

开发适配器时,请遵循以下原则:

  1. 保持无状态 — 适配器应该是一个轻量的协议转换层。所有会话状态存储在 cc-connect 中。
  2. 处理断线重连 — 网络故障是正常的,实现指数退避重试。
  3. 如实声明能力 — 只声明你的平台实际支持的能力。
  4. 忠实使用 reply_ctx — 始终原样回传原始消息中的 reply_ctx
  5. 二进制数据用 Base64 — 图片、文件和音频通过 base64 编码字符串传输。
  6. 记录错误而非崩溃 — 收到未知消息类型时,记录日志并继续运行。

最小适配器示例(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
  }
}