WebSocket 协议

1. WebSocket 简介

WebSocket 是一种在单个 TCP 连接上进行双向通信的协议,适合需要长连接、低延迟、服务端持续推送消息的场景。客户端先发起一个普通 HTTP 请求,服务端通过 Upgrade: websocket 完成协议升级;升级成功后,连接就不再按一次请求一次响应的 HTTP 模式工作,而是由客户端和服务端互相发送消息帧。

DolphinX 的 HTTP API 和 WebSocket API 使用同一组业务能力。区别在于:HTTP 通过 method + path + query + body 表达一次调用;WebSocket 连接建立后,业务调用通过 JSON 请求帧表达,其中 action 表示要调用的接口,payload 承载请求参数。

建议在以下场景使用 WebSocket:

  • 需要流式对话、实时展示模型输出或上下文处理事件。
  • 客户端需要复用同一个连接连续发送多次请求。
  • 需要用 requestId / correlationId 在同一连接上匹配多路请求和响应。

如果只是偶发查询、管理类操作或不需要实时流式返回,HTTP 调用通常更简单。

2. 连接

WebSocket 端点路径与 HTTP 相同,通过协议升级连接:

ws://<host>:<port>/agent-bus/v1
wss://<host>:<port>/agent-bus/v1

客户端推荐在握手时声明 agent-bus subprotocol,并通过 Authorization 携带登录后获得的 token:

Sec-WebSocket-Protocol: agent-bus
Authorization: Bearer <token>

通过 Python 发起 WebSocket 请求的示例:

import asyncio
import json
import websockets

async def main():
    token = "<token>"
    url = "ws://localhost:8848/agent-bus/v1"

    async with websockets.connect(
        url,
        additional_headers={"Authorization": f"Bearer {token}"},
        subprotocols=["agent-bus"],
    ) as ws:
        request = {
        "type": "REQUEST",
        "requestId": "req_001",
        "action": "agent.accessible",
   }

        await ws.send(json.dumps(request, ensure_ascii=False))
        response = await ws.recv()
        print(response)

asyncio.run(main())

3. 消息格式

所有 WebSocket 消息都是 JSON。

请求帧

客户端调用接口时发送 REQUEST 帧。请求帧只识别下列顶层参数,其它参数一律忽略:

参数 必填 说明
type 固定为 "REQUEST"。
action WebSocket action 名,见下方 Action 对照表。
requestId 否,但是建议填写 客户端生成的请求 ID,服务端会原样回填到响应帧的 correlationId。
agentId 视 action 而定 Agent ID。当对应 HTTP 路由含 {agentId} 或业务要求指定 Agent 时必填。
sessionId 视 action 而定 会话 ID。当对应 HTTP 路由含 {sessionId} 或业务要求指定会话时必填。
payload 视 action 而定 JSON 对象,承载 HTTP 请求体、查询串,以及除 agentId / sessionId 以外的路径参数。
{
  "type": "REQUEST",
  "requestId": "req_001",
  "action": "chat.completions",
  "agentId": "agent_xxx",
  "sessionId": "sess_001",
  "payload": {
    "message": "你好",
    "stream": true,
    "contextEvent": true
  }
}

响应帧

普通非流式响应会返回一条 RESPONSE 帧。客户端可用 correlationId 匹配请求中的 requestId

{
  "type": "RESPONSE",
  "msgId": "...",
  "correlationId": "req_001",
  "action": "chat.completions",
  "agentId": "agent_xxx",
  "sessionId": "sess_001",
  "data": {}
}

流式帧

流式请求会返回多条消息。每条消息都是独立 JSON,不会包装成 HTTP SSE 的 event: / data: 文本格式。客户端应持续读取,直到收到 STREAM_END 或 STREAM_ERROR。

{"type":"STREAM_START","correlationId":"req_001","action":"chat.completions","data":{"requestId":"req_001","model":"<resolved-model>"}}
{"type":"STREAM_REASONING","correlationId":"req_001","action":"chat.completions","data":{"reasoning":"..."}}
{"type":"STREAM_CHUNK","correlationId":"req_001","action":"chat.completions","data":{"content":"..."}}
{"type":"STREAM_END","correlationId":"req_001","action":"chat.completions","data":{"metadata":{}}}

WebSocket 对话补全支持 contextEvent=true。开启后,上下文压缩等过程会以 EVENT 消息返回,并使用同一个 correlationId

错误帧

{
  "type": "ERROR",
  "msgId": "...",
  "correlationId": "req_001",
  "action": "chat.completions",
  "agentId": "agent_xxx",
  "sessionId": "sess_001",
  "error": {
    "code": "BUS_SESSION_NOT_FOUND",
    "message": "session not found"
  },
  "headers": {
    "busErrorCode": "BUS_SESSION_NOT_FOUND",
    "httpStatus": "404"
  }
}

4. HTTP 请求转换为 WebSocket 请求

把 HTTP 请求转换成 WebSocket 请求时,只需要按下面步骤处理:

  1. 在 Action 对照表中找到 HTTP 接口对应的 action。
  2. 将 HTTP 路径里的 {agentId} / {sessionId} 放到请求帧顶层的 agentId / sessionId
  3. 将其它路径参数、查询参数和 JSON 请求体字段放到 payload
  4. 生成一个 requestId,发送完整 JSON 请求帧。
  5. 读取服务端返回消息,用响应里的 correlationId 匹配请求的 requestId

同一个字段不要在顶层和 payload 中同时出现且取值不同,否则服务端会拒绝请求。例如 agentId 已经放在顶层时,不要再在 payload.agentId 中传入另一个值。

示例 1:GET 请求,路径参数 + 查询参数

HTTP 请求:

GET /agent-bus/v1/session/sess_001/messages?startSeq=1&endSeq=20
Authorization: Bearer <token>

转换规则:

HTTP 中的位置 HTTP 值 WebSocket 中的位置
路径 sess_001 顶层 sessionId
查询参数 startSeq=1 payload.startSeq
查询参数 endSeq=20 payload.endSeq
接口 GET /session/{id}/messages action: "session.getMessages"

发送的 WebSocket 请求帧:

{
  "type": "REQUEST",
  "requestId": "req_42",
  "action": "session.getMessages",
  "sessionId": "sess_001",
  "payload": {
    "startSeq": 1,
    "endSeq": 20
  }
}}

示例 2:GET 请求,查询参数中的 agentId 放到顶层

HTTP 请求:

GET /agent-bus/v1/session/list?agentId=agent_xxx&limit=50&offset=0
Authorization: Bearer <token>

转换规则:

HTTP 中的位置 HTTP 值 WebSocket 中的位置
查询参数 agentId=agent_xxx 顶层 agentId
查询参数 limit=50 payload.limit
查询参数 offset=0 payload.offset
接口 GET /session/list action: "session.list"

发送的 WebSocket 请求帧:

{
  "type": "REQUEST",
  "requestId": "req_43",
  "action": "session.list",
  "agentId": "agent_xxx",
  "payload": {
    "limit": 50,
    "offset": 0
  }
}

示例 3:POST 请求,路径参数 + JSON 请求体

HTTP 请求:

POST /agent-bus/v1/workspace/sess_001/replace
Authorization: Bearer <token>
Content-Type: application/json

{
  "name": "main.dos",
  "hash": "xxh64:...",
  "replacements": [
    {
      "old": "return 1",
      "new": "return 2"
    }
  ]
}

转换规则:

HTTP 中的位置 HTTP 值 WebSocket 中的位置
路径 sess_001 顶层 sessionId
请求体 name payload.name
请求体 hash payload.hash
请求体 replacements payload.replacements
接口 POST /workspace/{sessionId}/replace action: "workspace.replace"

发送的 WebSocket 请求帧:

{
  "type": "REQUEST",
  "requestId": "req_44",
  "action": "workspace.replace",
  "sessionId": "sess_001",
  "payload": {
    "name": "main.dos",
    "hash": "xxh64:...",
    "replacements": [
      {
        "old": "return 1",
        "new": "return 2"
      }
    ]
  }
}

5. 支持的 Action

WebSocket 支持下表列出的 action。

Action 对应 HTTP 接口
agent.accessible GET /agent/accessible
agent.get GET /agent/{id}
agent.llm.selectable GET /agent/{id}/llm/selectable
session.create POST /session/create
session.get GET /session/{id}
session.update PUT /session/{id}
session.delete DELETE /session/{id}
session.list GET /session/list
session.getMessages GET /session/{id}/messages
session.generateSummary POST /session/{id}/summary
chat.completions POST /chat/completions
skill.catalog GET /skill/{agentId}/catalog
skill.getInstruction GET /skill/{id}/instruction
skill.files.list GET /skill/{id}/files/list
skill.files.get GET /skill/{id}/files/get
skill.files.search POST /skill/{id}/files/search
skill.files.batchGet POST /skill/{id}/files/batchGet
workspace.list GET /workspace/{sessionId}/files
workspace.read GET /workspace/{sessionId}/file
workspace.write POST /workspace/{sessionId}/file
workspace.delete DELETE /workspace/{sessionId}/file
workspace.search POST /workspace/{sessionId}/search
workspace.replace POST /workspace/{sessionId}/replace
network.fetch POST /network/fetch
mcp.tools.call POST /mcp/tools/call
context.preview POST /context/preview
llm.complete POST /llm/complete