> This is a page from the ElevenLabs documentation. For a complete page index, fetch https://elevenlabs.io/docs/llms.txt. For the full documentation in a single file, fetch https://elevenlabs.io/docs/llms-full.txt.

# Graph 呼叫机器人

## 概述

此方案让智能体成为一个**可呼叫的 Teams 身份**。用户可以按名称搜索并与其进行 1 对 1 通话，智能体会实时接听——无需电话号码、PSTN 或 Communications Credits。这是唯一可按名称呼叫的方案，也是部署起来最复杂的方案。

它使用 **Microsoft Graph 实时媒体机器人**（Cloud Communications 呼叫平台）。媒体 SDK（`Microsoft.Skype.Bots.Media`）**仅支持 Windows Server 上的 .NET**——在 Teams 通话中处理原始音频没有 Linux 或非 .NET 方案。

> **Note**
>
> 这是 Teams 中唯一可**按名称**呼叫的方案。如需更轻量的设置，请优先选择 [widget 选项卡](/docs/zh/eleven-agents/phone-numbers/microsoft-teams/widget-tab)；如果你明确需要电话号码，请使用
> [ACS](/docs/zh/eleven-agents/phone-numbers/microsoft-teams/azure-communication-services)。

## 工作原理

![Teams 用户按名称呼叫机器人；Teams 将通话路由到 Windows VM 上的媒体机器人，后者通过 WebSocket 将原始 PCM 16k 音频桥接至 ElevenLabs 智能体](/docs/_fern-files/elevenlabs.docs.buildwithfern.com/f411ec0b44d4c5ca1a61598fbd1e6162e46a1c74179eebb4b673ee1428823e84/assets/images/conversational-ai/teams-media-bot-architecture.svg)

机器人通过**应用托管媒体**接听，每秒接收 50 个音频帧（20 ms PCM 16 kHz），通过 WebSocket 将其桥接至 ElevenLabs 智能体，并将智能体音频流式传回通话中。

## 要求

1. 一个 **Azure Bot** 注册和应用（Entra 应用注册）。
2. 已获管理员同意的 **Graph 应用程序权限**：`Calls.AccessMedia.All`（原始媒体）以及 `Calls.Initiate.All`。
3. 一个 **Windows Server VM**（≥ **2 个物理核心**，例如 `Standard_D4s_v3`），具有公共 IP 和开放的媒体端口。
4. 为媒体/信令端点配置公共 FQDN 上的 **CA 签名 TLS 证书**（媒体平台拒绝自签名证书）。
5. 一个[ ElevenLabs 智能体](/docs/zh/eleven-agents/quickstart)，两端均设为 **PCM 16000 Hz**：在 **Voice** 选项卡设置 TTS 输出格式，在 **Advanced** 选项卡设置用户输入音频格式。

> **Warning**
>
> `D2s_v3`（2 vCPU = **1 个物理核心**）会因 `MediaPlatform needs a system with at least 2
>   cores` 而失败。请使用至少有 2 个物理核心的规格（例如 `D4s_v3`）。

## 权限和角色

| 范围                    | 角色 / 权限                                                                                      | 原因                                 |
| --------------------- | -------------------------------------------------------------------------------------------- | ---------------------------------- |
| Entra                 | **应用程序管理员**                                                                                  | 创建应用注册和 Azure Bot                  |
| Entra                 | **全局管理员 / 特权角色管理员**                                                                          | 为 Graph 通话权限授予管理员同意 — 应用权限无法自行获得同意 |
| Microsoft Graph（应用程序） | `Calls.AccessMedia.All`、`Calls.Initiate.All`                                                 | 接听 1 对 1 通话并访问原始媒体                 |
| Azure RBAC            | 资源组的 **参与者**                                                                                 | 创建 Windows VM 和 Azure Bot          |
| Teams 管理员             | 允许[**上传自定义应用**](https://admin.teams.microsoft.com/policies/manage-apps)；启用机器人 **Calling** 通道 | 旁加载应用并接听来电                         |

## 第 1 步 — 注册机器人和 Graph 权限

创建应用注册并将 Azure Bot 绑定到该注册，然后授予并同意通话权限（同意需要全局管理员 / 特权角色管理员）：

```bash
APPID=$(az ad app create --display-name "ElevenLabs Teams Agent" \
  --sign-in-audience AzureADMyOrg --query appId -o tsv)
az ad sp create --id "$APPID"
# create a client secret and record it
az ad app credential reset --id "$APPID" --display-name bot --query password -o tsv

# Azure Bot bound to the app
az bot create --resource-group $RG --name el-teams-agent-bot \
  --app-type SingleTenant --appid "$APPID" --tenant-id $TENANT \
  --endpoint "https://YOUR_FQDN/api/messages" --sku S1
```

授予两个 Graph 应用程序角色并进行**管理员同意**（需要全局管理员 / 特权角色管理员），然后确认已分配成功：

```bash
# Graph app roles: Calls.AccessMedia.All, Calls.Initiate.All
az ad app permission add --id "$APPID" --api 00000003-0000-0000-c000-000000000000 \
  --api-permissions a7a681dc-756e-4909-b988-f160edc6655f=Role \
    284383ee-7f6e-4e40-a2a8-e85dcb029101=Role
az ad app permission admin-consent --id "$APPID"

# Verify — should print both role ids
az rest --method GET \
  --url "https://graph.microsoft.com/v1.0/servicePrincipals(appId='$APPID')/appRoleAssignments" \
  --query "value[].appRoleId" -o tsv
```

> **Note**
>
> 如果 `admin-consent` 返回 `Consent validation failed`，请改为直接在服务主体上授予应用角色：
>
> ```bash
> GRAPH_SP=$(az ad sp show --id 00000003-0000-0000-c000-000000000000 --query id -o tsv)
> BOT_SP=$(az ad sp show --id "$APPID" --query id -o tsv)
> for ROLE in a7a681dc-756e-4909-b988-f160edc6655f 284383ee-7f6e-4e40-a2a8-e85dcb029101; do
>   az rest --method POST \
>     --url "https://graph.microsoft.com/v1.0/servicePrincipals/$GRAPH_SP/appRoleAssignedTo" \
>     --body "{\"principalId\": \"$BOT_SP\", \"resourceId\": \"$GRAPH_SP\", \"appRoleId\": \"$ROLE\"}"
> done
> ```

在门户的 [Entra 管理中心](https://entra.microsoft.com/#view/Microsoft_AAD_RegisteredApps/RegisteredAppsListBlade)中，依次进入**应用注册 → 应用 → API 权限**进行验证：两个权限都应显示为**已授予**，并带有绿色勾选标记。

![应用注册的 API 权限面板，显示 Calls.AccessMedia.All 和 Calls.Initiate.All
已授予](/docs/_fern-img/4abc1e220d3fc03845594aa4f835656425cc1c39448dae30dace98e672fd8fb1.webp)

## 第 2 步 — 配置 Windows VM、证书和端口

```bash
az vm create -g $RG -n teams-media-bot --image Win2022Datacenter \
  --size Standard_D4s_v3 --admin-username azureuser --admin-password '<strong-pw>' \
  --public-ip-sku Standard --public-ip-address-dns-name elevenmediabot
az vm open-port -g $RG -n teams-media-bot --port 80,443,8445,9441 --priority 300
```

在 VM 上执行以下操作（媒体平台的原生代码需要这些组件，而 Windows Server 默认不包含它们）：

```powershell
# VC++ runtime + Media Foundation feature (required by NativeMedia.dll)
choco install -y vcredist140
Install-WindowsFeature Server-Media-Foundation

# CA cert for the VM's FQDN via win-acme (HTTP-01), then import to LocalMachine\My
& wacs.exe --target manual --host <vm-fqdn>.cloudapp.azure.com `
  --validation selfhosting --store pfxfile --pfxfilepath C:\bot\certs --accepttos
```

同时在 **Windows 防火墙**中开放相同端口，并记录证书**指纹** — 机器人会将 Kestrel（443 + 通知端口）和媒体平台（8445）绑定到该证书。

> **Tip**
>
> VM 自身的 `*.cloudapp.azure.com` FQDN 可用于 Let's Encrypt 证书，无需单独的域名。

## 第 3 步 — 构建并运行机器人

从 Microsoft 的 [`microsoft-graph-comms-samples`](https://github.com/microsoftgraph/microsoft-graph-comms-samples) **PublicSamples/EchoBot** 开始 — 它以 `net6.0` 为目标框架，可通过 .NET SDK 构建（无需 Visual Studio Build Tools）：

```powershell
git clone --depth 1 https://github.com/microsoftgraph/microsoft-graph-comms-samples.git C:\bot\samples
cd C:\bot\samples\Samples\PublicSamples\EchoBot\src
dotnet build EchoBot.sln -c Release
```

在 `appsettings.json` 的 `AppSettings` 部分配置 `AadAppId`、`AadAppSecret`、`ServiceDnsName`/`MediaDnsName`（VM FQDN）、`CertificateThumbprint` 和端口（通话 `443`、通知 `9441`、媒体 `8445`）。为下方 ElevenLabs 桥接添加两个设置：`ElevenLabsAgentId` 和 `ElevenLabsOrigin`（`wss://api.elevenlabs.io`，或你的数据驻留主机）。将其作为 Windows 计划任务 / 服务运行，以便在重启后持续运行。

> **Warning**
>
> 任务计划程序默认的**执行时间限制（72 小时）** 会悄然终止长期运行的任务 — 开机时启动的机器人会在 3 天后停止，通话将失败并提示 "we couldn't connect you"。请禁用此限制并添加失败后重启：
>
> ```powershell
> $s = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Seconds 0) `
>   -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) -StartWhenAvailable
> Set-ScheduledTask -TaskName EchoBot -Settings $s
> ```

> **Warning**
>
> 原版 EchoBot 在拨打标准端口 443 的通话时会崩溃：`HttpHelpers.SetAbsoluteUri`
> 会调用 `req.Host.Port.Value`，当 Host 标头未明确指定端口时，该值为 null。请将其修复为
> `req.Host.Port ?? (req.IsHttps ? 443 : 80)`。

### 将回声替换为 ElevenLabs

EchoBot 的音频接口很清晰：`SpeechService.AppendAudioBuffer(in)` 和 `OnSendMediaBufferEventArgs(out)` 事件。将其 Azure-Speech 主体替换为 ElevenLabs 智能体 WebSocket 桥接，并保持相同接口：

**`SpeechService.cs — ElevenLabs 桥接（核心）`**

```csharp title="SpeechService.cs — ElevenLabs 桥接（核心）"
public class SpeechService
{
    private readonly AppSettings _settings;
    private readonly ILogger _logger;
    private ClientWebSocket _ws;
    private bool _started;
    private bool _connecting;

    public event EventHandler<MediaStreamEventArgs> SendMediaBuffer; // agent audio -> call
    public event EventHandler FlushMedia;                            // barge-in: drop queued audio

    public SpeechService(AppSettings settings, ILogger logger) { _settings = settings; _logger = logger; }

    // Caller audio -> ElevenLabs
    public async Task AppendAudioBuffer(AudioMediaBuffer buffer)
    {
        if (!_started)
        {
            if (_connecting) return;            // a connect attempt is already in flight
            _connecting = true;
            try { await Connect(); _started = true; }
            catch (Exception ex) { _logger.Error(ex, "ElevenLabs connect failed; retry on next frame"); return; }
            finally { _connecting = false; }
        }
        if (_ws?.State != WebSocketState.Open || buffer.Length <= 0) return;
        var pcm = new byte[buffer.Length];
        Marshal.Copy(buffer.Data, pcm, 0, (int)buffer.Length);
        var msg = JsonSerializer.Serialize(new { user_audio_chunk = Convert.ToBase64String(pcm) });
        await _ws.SendAsync(Encoding.UTF8.GetBytes(msg), WebSocketMessageType.Text, true, default);
    }

    private async Task Connect()
    {
        _ws = new ClientWebSocket();
        // ElevenLabsOrigin: wss://api.elevenlabs.io, or a residency host (.eu./.in./.sg.)
        var url = $"{_settings.ElevenLabsOrigin}/v1/convai/conversation?agent_id={_settings.ElevenLabsAgentId}";
        await _ws.ConnectAsync(new Uri(url), default);
        await _ws.SendAsync(Encoding.UTF8.GetBytes(
            JsonSerializer.Serialize(new { type = "conversation_initiation_client_data" })),
            WebSocketMessageType.Text, true, default);
        _ = Task.Run(ReceiveLoop);
    }

    private async Task ReceiveLoop()
    {
        var buf = new byte[32768]; var sb = new StringBuilder();
        while (_ws.State == WebSocketState.Open)
        {
            sb.Clear(); WebSocketReceiveResult r;
            do { r = await _ws.ReceiveAsync(buf, default); sb.Append(Encoding.UTF8.GetString(buf, 0, r.Count)); }
            while (!r.EndOfMessage);

            using var doc = JsonDocument.Parse(sb.ToString());
            var type = doc.RootElement.GetProperty("type").GetString();
            if (type == "audio")            // ElevenLabs audio -> call
                Emit(Convert.FromBase64String(doc.RootElement
                    .GetProperty("audio_event").GetProperty("audio_base_64").GetString()));
            else if (type == "ping")
                await _ws.SendAsync(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new {
                    type = "pong", event_id = doc.RootElement.GetProperty("ping_event").GetProperty("event_id").GetInt32() })),
                    WebSocketMessageType.Text, true, default);
            else if (type == "interruption")  // barge-in: drop any agent audio still queued
                FlushMedia?.Invoke(this, EventArgs.Empty);
        }
    }

    // slice PCM into 20 ms / 640-byte frames the media platform expects
    private void Emit(byte[] pcm)
    {
        var all = new List<AudioMediaBuffer>(); long tick = DateTime.Now.Ticks;
        for (int off = 0; off < pcm.Length; off += 640)
        {
            var frame = new byte[640];
            Array.Copy(pcm, off, frame, 0, Math.Min(640, pcm.Length - off));
            all.AddRange(Utilities.CreateAudioMediaBuffers(frame, tick, _logger));
            tick += 20 * 10000;
        }
        if (all.Count > 0) SendMediaBuffer?.Invoke(this, new MediaStreamEventArgs { AudioMediaBuffers = all });
    }
}
```

两端都是 PCM 16 kHz 单声道，因此可直接透传 base64 — 将智能体设为 `pcm_16000`。当 ElevenLabs 发出 `interruption`（插话）时，桥接会触发 `FlushMedia`；将其连接到媒体流，以丢弃所有排队的 `AudioMediaBuffer`，否则智能体会继续覆盖来电者讲话。完整消息参考请见 [WebSocket 文档](/docs/zh/eleven-agents/libraries/web-sockets)。通话结束挂断和暖转接将在下方章节说明。

> **Note**
>
> `Connect()` 中的 URL 会连接到**公开**智能体。对于私有智能体，请在服务器端请求短期有效的
> 签名 URL — 使用 API 密钥调用 `GET /v1/convai/conversation/get-signed-url?agent_id=...` — 然后连接到返回的 URL。使用[数据驻留](/docs/zh/overview/administration/data-residency)时，将 `ElevenLabsOrigin` 设为相应的数据驻留主机（`wss://api.eu.residency.elevenlabs.io`、`.in.` 或 `.sg.`）— 签名 URL 请求使用对应的 `https://` 主机。

## 第 4 步 — 使其可在 Teams 中呼叫

1. 在 Azure Bot 的 Teams 通道中**启用 Calling**，并将**通话 webhook** 设为 `https://YOUR_FQDN/api/calling`：

   ```bash
   az bot msteams create -g $RG -n el-teams-agent-bot \
     --enable-calling --calling-web-hook "https://YOUR_FQDN/api/calling"
   ```

   在门户中，路径为 Azure Bot 资源 → **Channels** → **Microsoft Teams** → **Calling** 选项卡：

   ![Azure Bot Channels 面板列出状态正常的 Microsoft Teams 通道](/docs/_fern-img/01115d26ab645dd6ec6569a36a918cd4de0214c79dcb0070d7a3c3a5e544c93b.webp)

   ![Teams 通道的 Calling 选项卡，已勾选 Enable calling 并设置通话 webhook](/docs/_fern-img/7c4819f64878bfb152661adc948c1170f9efd48ba3d68401c4a4d4ba075951b0.webp)

2. 使用 `bots[0].supportsCalling: true` 和机器人的应用 ID 构建 **Teams 应用清单**，然后旁加载该应用（**Apps → Manage your apps → Upload a custom app**），或者不通过 UI 将其发布到整个组织：`New-TeamsApp -DistributionMethod organization -Path ./bot-app.zip`（MicrosoftTeams PowerShell 模块）。

在 Teams 中按名称搜索该应用并呼叫它 — 机器人会接听，ElevenLabs 智能体将开始说话。

![与 ElevenLabs 智能体机器人的活跃 Teams 通话](/docs/_fern-img/0f1fad08a091bcd20ffbbf5c3949b5503f423d8eb6298f464a870b3a0a254726.webp)

> **Note**
>
> 按名称进行 1 对 1 呼叫无需电话号码或资源账户 — 这些仅用于 PSTN 拨入。`Calls.AccessMedia.All` 可启用原始音频桥接。

## 文本聊天（同一机器人）

同一个 Azure Bot 也可以在 Teams 中回复**文本** — 用户既可以呼叫智能体，也可以与其聊天。通话和消息是机器人上的独立通道：通话 webhook 处理语音，Bot Framework **消息终结点**（`/api/messages`）处理聊天。

![与 ElevenLabs 智能体机器人进行 Teams 聊天，它正在回复文本消息](/docs/_fern-img/fb75ae17848916790d29c1c0a703051946902893e46ed62884742db36be453b2.webp)

将机器人的消息终结点指向提供该服务的任意主机（媒体机器人或其他服务 — 不必是 Windows VM）：

```bash
az bot update -g $RG -n el-teams-agent-bot --endpoint "https://YOUR_FQDN/api/messages"
```

使用 Bot Framework SDK 实现该终结点，并通过与语音相同的会话 WebSocket，以**文本模式**将每条消息转发给智能体 — 发送 [`user_message`](/docs/zh/eleven-agents/customization/events/client-to-server-events) 事件，读取 [`agent_response`](/docs/zh/eleven-agents/customization/events/client-events) 事件。首先在智能体[覆盖设置](/docs/zh/eleven-agents/customization/personalization/overrides)中启用**首条消息**字段 — 下方代码将其覆盖为空，使回复回答用户消息而不是智能体问候语：

**`ChatBot.cs — Teams 文本聊天 -> ElevenLabs（文本模式）`**

```csharp title="ChatBot.cs — Teams 文本聊天 -> ElevenLabs（文本模式）"
public class ChatBot : ActivityHandler
{
    private readonly AppSettings _settings;
    public ChatBot(AppSettings settings) => _settings = settings;

    protected override async Task OnMessageActivityAsync(
        ITurnContext<IMessageActivity> turn, CancellationToken ct)
    {
        var reply = await AskAgent(turn.Activity.Text, ct);
        await turn.SendActivityAsync(MessageFactory.Text(reply), ct);
    }

    private async Task<string> AskAgent(string text, CancellationToken ct)
    {
        using var ws = new ClientWebSocket();
        var url = $"{_settings.ElevenLabsOrigin}/v1/convai/conversation?agent_id={_settings.ElevenLabsAgentId}";
        await ws.ConnectAsync(new Uri(url), ct);
        // Suppress the agent's greeting: with no override, the first agent_response is the
        // configured first message, not the answer to this user_message.
        await Send(ws, new
        {
            type = "conversation_initiation_client_data",
            conversation_config_override = new { agent = new { first_message = "" } },
        }, ct);
        await Send(ws, new { type = "user_message", text }, ct);

        var buf = new byte[16384]; var sb = new StringBuilder();
        while (ws.State == WebSocketState.Open)
        {
            sb.Clear(); WebSocketReceiveResult r;
            do { r = await ws.ReceiveAsync(buf, ct); sb.Append(Encoding.UTF8.GetString(buf, 0, r.Count)); }
            while (!r.EndOfMessage);

            using var doc = JsonDocument.Parse(sb.ToString());
            switch (doc.RootElement.GetProperty("type").GetString())
            {
                case "agent_response":
                    return doc.RootElement.GetProperty("agent_response_event")
                              .GetProperty("agent_response").GetString();
                case "ping":
                    await Send(ws, new { type = "pong", event_id = doc.RootElement
                        .GetProperty("ping_event").GetProperty("event_id").GetInt32() }, ct);
                    break;
            }
        }
        return "Sorry, I couldn't reach the agent.";
    }

    private static Task Send(ClientWebSocket ws, object msg, CancellationToken ct) =>
        ws.SendAsync(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(msg)),
            WebSocketMessageType.Text, true, ct);
}
```

按标准方式注册（使用 `CloudAdapter`，通过 `AddTransient<IBot, ChatBot>()` 注册机器人，并添加 `/api/messages` 控制器），然后将聊天范围添加到清单的机器人条目：

```json
"bots": [
  { "botId": "YOUR_APP_ID", "supportsCalling": true, "scopes": ["personal", "team", "groupChat"] }
]
```

> **Note**
>
> 此代码片段会为每条消息打开一个新会话，因此每轮对话彼此独立。若需聊天**记忆**，请为每个 Teams `conversation.id` 保持一个 WebSocket 连接（跨轮次复用），并清理闲置会话 — 智能体便会记住该聊天中的早期消息。必须在智能体的[覆盖设置](/docs/zh/eleven-agents/customization/personalization/overrides)中启用 `first_message` 覆盖 — 如果发送不允许的覆盖，服务器会**关闭会话**。如果无法启用，请省略该覆盖，改为丢弃每个会话的第一条 `agent_response`（问候语），并返回下一条。

> **Tip**
>
> 如果始终收不到聊天回复，请在智能体的**高级**设置中启用 **`agent_response`** [客户端事件](/docs/zh/eleven-agents/customization/events/client-events) — 文本回复通过此事件传送。

## 通话结束

当 ElevenLabs 结束会话时（其**结束通话**工具会关闭 WebSocket），请挂断 Teams 通话：

```csharp
await this.Call.DeleteAsync(); // after a short delay so the goodbye audio finishes
```

## 暖转接给人工客服

智能体会触发自定义 `transfer_to_human` 客户端工具；机器人会将 **Teams 用户邀请**加入进行中的通话（协商式添加），然后退出：

```csharp
var target = new IdentitySet { User = new Identity { Id = humanObjectId } };
await this.Call.Participants.InviteAsync(target, replacesCallId: null);
// suppress the end-call hangup while transferring, and mute the bot
```

> **Tip**
>
> 协商式转接（`replacesCallId`）要求双方都是**同一租户**中的 Teams 用户；PSTN 转接目标需要应用实例。若要先向人工客服说明情况，请从智能体传入 `reason` 参数，并在桥接前播放给人工客服。

## 故障排除

#### \`MediaPlatform needs a system with at least 2 cores\`

VM 只有一个物理核心。请调整为 ≥ 2 个物理核心（例如 `D4s_v3`），然后重启。

#### \`Unable to load DLL 'NativeMedia'\`

安装 **VC++ Redistributable**（`vcredist140`）和 **Server-Media-Foundation** Windows 功能，然后重启机器人。

#### 来电返回 500 / 通话无法连接

这是 EchoBot 在端口 443 上的空端口 bug — 请修复 `HttpHelpers.SetAbsoluteUri`（参见第 3 步）。同时确认该证书由 CA 签发，且可通过端口 443 访问。

#### 呼叫机器人时提示“we couldn't connect you”

确认 Teams 通道已启用 Calling 并配置正确的 `/api/calling` webhook，Graph `Calls.AccessMedia.All` 权限已获同意，且 NSG 和 Windows 防火墙均已开放端口 443/8445/9441。如果呼叫**之前可用但后来停止**，请检查机器人进程是否仍在 VM 上运行 — 任务计划程序默认的 72 小时执行限制会在开机数天后将其终止（参见第 3 步中的警告）。

## 实用链接

* [通话和会议机器人概览](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/calls-and-meetings/calls-meetings-bots-overview)
* [实时媒体概念](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/calls-and-meetings/real-time-media-concepts)
* [注册通话机器人](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/calls-and-meetings/registering-calling-bot)
* [microsoft-graph-comms-samples（EchoBot）](https://github.com/microsoftgraph/microsoft-graph-comms-samples)
* [call: transfer（协商式）](https://learn.microsoft.com/en-us/graph/api/call-transfer)
* [智能体 WebSocket 协议](/docs/zh/eleven-agents/libraries/web-sockets)