
# WebSocket 接入

*最通用的实时音频通道：连接、控制消息、事件、音频格式*

WebSocket 是最简单通用的接入方式：建立一条全双工连接，**二进制帧**发送麦克风音频，**文本帧（JSON）**收发控制消息与事件。适合自定义客户端、IoT、服务器中转。

## 1. 建立连接

`WS wss://hearo-gw.apps.aisp24.com/v1/connect?token=<客户端 Token>&...`

> ℹ️ **本部署的网关地址**：`wss://hearo-gw.apps.aisp24.com/v1/connect`（下方示例已代入，可直接复制使用；地址由平台管理员在 运营中心 → 平台设置 → 接入地址 配置）。

查询参数（除 `token` 外均可选）：

| 参数 | 默认 | 说明 |
|---|---|---|
| token | 必填 | 客户端 Token（见 [鉴权与 Token](/docs/auth)） |
| encoding | pcm_s16le | 上行编码：`pcm_s16le` \| `opus`（别名 `pcm`/`pcm16`/`l16` 同 pcm_s16le） |
| sample_rate | 16000 | 上行采样率。pcm_s16le：16000 / 24000 / 48000；opus：必须 48000 |
| channels | 1 | 声道数：1 或 2 |
| frame_duration_ms | 20 | 帧时长：10 / 20 / 40 |
| downlink_codec | pcm_s16le | 下行编码：`pcm_s16le` \| `opus` |
| name | — | 在房间花名册（roster）中显示的昵称 |
| lang | — | 你说的语言（如 `zh-CN` / `en`），供字幕/翻译使用（见 [实时字幕](/docs/captions)） |
| caption_targets | — | 连接即开启本路字幕，翻译为这些语言（逗号分隔）；也可连接后用 `caption_start` 开启 |
| session_id | 自动生成 | 会话标识，断线重连时带上同一个值 |
| resume_from_seq | 0 | 重连续传的起始帧序号（配合 session_id） |

```javascript
const url =
  "wss://hearo-gw.apps.aisp24.com/v1/connect" +
  "?token=" + encodeURIComponent(token) +
  "&encoding=pcm_s16le&sample_rate=16000&channels=1" +
  "&frame_duration_ms=20&downlink_codec=pcm_s16le";

const ws = new WebSocket(url);
ws.binaryType = "arraybuffer";
```

连接成功后，服务端先发一条 `ready` 文本帧（回显协商结果）：

```json
{ "type": "ready", "session_id": "...", "resume_from_seq": 0,
  "encoding": "pcm_s16le", "sample_rate": 16000, "channels": 1,
  "frame_duration_ms": 20 }
```

进入哪个房间由平台的 **client→room 映射**决定（Token 里不含房间），见 [客户端与接入模型](/docs/clients)。

## 2. 发送音频（二进制帧）

按协商好的格式分帧、作为**二进制 WebSocket 帧**发送即可。默认格式（16kHz / 单声道 / s16le）下 20ms 一帧 = 320 采样 = 640 字节。选 `encoding=opus` 时发送 48kHz 单声道 Opus 包。无论上行是什么格式，gateway 都会在边缘解码/重采样成统一的 **16k 单声道 PCM** 再进房间。

> 💡 **按 ~1x 实时节奏发送**：不要把整段音频一次性灌入；按帧、按实时节奏发送，打断（barge-in）和延迟才正常。

## 3. 控制消息（客户端 → 服务端，文本帧）

gateway 只在本地处理少数几种控制消息，其余**原样透传**给房间和房间里的 AI 智能体：

| type | 处理方 | 说明 |
|---|---|---|
| ping | gateway | 心跳，服务端回 `{"type":"pong"}` |
| caption_start / caption_stop | gateway | 开/关本路实时字幕（见 [实时字幕](/docs/captions)） |
| audio | gateway | 可选的二进制帧头 `{"type":"audio","seq":...}`：为紧随其后的二进制帧标注序号 |
| config | gateway + 智能体 | 更新音频格式 / 实时调智能体模式与 VAD（见 [交互模式与 VAD](/docs/modes)） |
| speech_start / turn_start | 智能体 | 开始说话/新轮次：打断当前回答并开始新一轮录音 |
| speech_end | 智能体 | 结束说话（PTT 松手）：停止送入 ASR 并收尾本句 |
| commit | 智能体 | 显式收尾当前话语（PTT 释放） |
| interrupt | 智能体 | 立即取消 AI 当前回答（LLM+TTS） |

```javascript
ws.send(JSON.stringify({ type: "interrupt" }));     // 打断 AI
ws.send(JSON.stringify({ type: "speech_start" }));  // 开始新一轮
```

> ℹ️ **交互模式决定该发哪些控制**：按住说话(ptt) 用 speech_start/speech_end；自然对话(natural) 由服务端 VAD 自动判断，通常无需手动发。详见 [交互模式与 VAD](/docs/modes)。

参与者发送的 `kick` 控制不会执行。精确断开连接属于服务端运营权限，
只能通过 Console 到 Room 的私有、鉴权操作接口发起。

## 4. 事件（服务端 → 客户端）

文本帧为 JSON 事件，按 `type` 区分：

| type | 字段 | 说明 |
|---|---|---|
| transcript | text, is_final, confidence | ASR 识别结果；is_final 前为流式部分结果 |
| caption | id, speaker, source_lang, text, translations, is_final | 实时字幕/翻译（见 [实时字幕](/docs/captions)） |
| turn_started | turn_id | AI 开始回答 |
| assistant_delta | turn_id, delta | LLM 流式文本增量 |
| audio | turn_id, seq, sample_rate, channels, encoding, bytes | 紧随其后的二进制音频帧的头 |
| turn_complete | turn_id, text | AI 回答结束 |
| turn_cancelled | turn_id, reason | 被打断/中断（reason=barge_in 等） |
| session_idle | — | 多轮模式空闲超时 |
| roster | members[] | 房间成员变化（`{id,name,kind:"human"\|"agent"}`），进房即收到一份全量快照 |
| kicked | — | 你被踢出了房间 |
| pong | — | 对 ping 的应答 |
| error | message | 错误 |

**下行音频**：每段合成音频先来一条 `{type:"audio", ...}` 文本头，紧接着一个**二进制帧**（按 `downlink_codec` 编码，默认 16k PCM）。

```javascript
let pendingAudio = null;
ws.onmessage = (ev) => {
  if (typeof ev.data === "string") {
    const msg = JSON.parse(ev.data);
    if (msg.type === "audio") pendingAudio = msg;          // 头
    else if (msg.type === "transcript") render(msg.text);
    else if (msg.type === "roster") renderMembers(msg.members);
    else if (msg.type === "turn_cancelled") stopPlayback(); // 打断：停播
  } else {
    play(ev.data, pendingAudio);   // 紧随头的二进制音频
    pendingAudio = null;
  }
};
```

## 5. 断线重连

重连时带上同一个 `session_id`，并把 `resume_from_seq` 设为你已成功发送的最后帧序号，服务端从该序号之后续收、避免重复帧。简单客户端也可以不管这两个参数——直接重连即可（按新会话处理）。

## 6. 邀请 AI

连上房间后，AI 智能体由控制面调度进房间（不是通过这条 WS）：调用 `POST /api/v1/agents`（见 [REST API](/docs/rest-api)）、在控制台「演示」页点「邀请 AI 加入」、或给房间配**默认智能体**（进房自动就位，见 [房间与分组](/docs/rooms)）。AI 进来后会出现在 `roster` 里（kind=agent）。

> 💡 **对照实时调试台**：控制台的「演示」页就是一个完整的 WebSocket 客户端参考实现（含重采样、分帧、播放、打断），可边读代码边对照行为。
