# 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://:/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": "", "format": "mp3" } ``` #### `image` 发送图片给用户。仅在适配器声明了 `"image"` 能力时发送。 ```json { "type": "image", "session_key": "wechat:user123:user123", "reply_ctx": "conv-abc-123", "data": "", "mime_type": "image/png", "file_name": "screenshot.png" } ``` #### `file` 发送文件给用户。仅在适配器声明了 `"file"` 能力时发送。 ```json { "type": "file", "session_key": "wechat:user123:user123", "reply_ctx": "conv-abc-123", "data": "", "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": "", "file_name": "screenshot.png" } ``` ### 文件对象 ```json { "mime_type": "application/pdf", "data": "", "file_name": "report.pdf" } ``` ### 音频对象 ```json { "mime_type": "audio/ogg", "data": "", "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 } } ```