> For the complete documentation index, see [llms.txt](https://docs.convai.com/api-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.convai.com/api-docs/zh/cha-jian-yu-ji-cheng/web-plugins/convai-web-sdk/cuo-wu-chu-li.md).

# 错误处理

<table><thead><tr><th>通道</th><th>事件</th><th width="286.31640625">触发时机</th></tr></thead><tbody><tr><td>传输错误</td><td><code>错误</code></td><td>底层 WebRTC / WebSocket 异常</td></tr><tr><td>会话结束</td><td><code>断开连接</code></td><td>每次会话结束时，附带原因代码</td></tr><tr><td>服务器确认</td><td><code>serverResponse</code></td><td>在你发送的每条消息之后</td></tr><tr><td>静默 LLM</td><td><code>llmNoResponse</code></td><td>LLM 明确选择不响应</td></tr><tr><td>空闲超时</td><td><code>idleWarning</code></td><td>服务器即将断开一个空闲会话</td></tr></tbody></table>

***

### `错误` 事件

触发于底层传输异常。载荷是 `未知` 因为它包装了底层传输抛出的任何内容。

```ts
client.on('error', (err) => {
  if (err instanceof Error) {
    console.error(err.name, err.message);
  } else {
    console.error(err);
  }
});
```

WebRTC 传输上的常见原因：

| 代码 | 含义                                    |
| -- | ------------------------------------- |
| 1  | `ConnectionError` — 权限被拒绝、服务器不可达或已取消  |
| 13 | `NegotiationError` — WebRTC 协商失败      |
| 21 | `DeviceUnsupportedError` — 麦克风或摄像头不可用 |

***

### `断开连接` 事件

会在每次会话结束时触发——无论是有意还是无意。载荷是一个数值 `DisconnectReason` 代码。

#### React

```tsx
import { useConvaiClient, DisconnectReasonEnum, getDisconnectReasonMessage } from '@convai/web-sdk';
import { useEffect } from 'react';

function App() {
  const client = useConvaiClient({ apiKey: '...', characterId: '...' });

  useEffect(() => {
    return client.on('disconnect', (reason) => {
      console.log(getDisconnectReasonMessage(reason));

      switch (reason) {
        case DisconnectReasonEnum.CLIENT_INITIATED:
          // 用户点击了断开连接——无需处理
          break;

        case DisconnectReasonEnum.SIGNAL_CLOSE:
        case DisconnectReasonEnum.UNKNOWN_REASON:
        case DisconnectReasonEnum.STATE_MISMATCH:
          // 网络中断——重试
          retryWithBackoff(() => client.reconnect());
          break;

        case DisconnectReasonEnum.SERVER_SHUTDOWN:
          setTimeout(() => client.reconnect(), 5000);
          break;

        case DisconnectReasonEnum.DUPLICATE_IDENTITY:
          // 另一个标签页 / 设备接管了——不要自动重试
          alert('会话已在另一个窗口中打开。');
          break;

        case DisconnectReasonEnum.JOIN_FAILURE:
          // 配置问题——向用户提示，不要循环重试
          alert('加入失败。请检查你的 API key 和角色 ID。');
          break;
      }
    });
  }, [client]);
}
```

#### 原生 JS

```ts
import { ConvaiClient, DisconnectReasonEnum, getDisconnectReasonMessage } from '@convai/web-sdk/core';

const client = new ConvaiClient({ apiKey: '...', characterId: '...' });

client.on('disconnect', (reason) => {
  console.log(getDisconnectReasonMessage(reason));

  if (reason !== DisconnectReasonEnum.CLIENT_INITIATED) {
    retryWithBackoff(() => client.reconnect());
  }
});
```

#### 原因代码参考

| 代码 | 枚举键                   | 含义                        | 自动重试？    |
| -- | --------------------- | ------------------------- | -------- |
| 0  | `UNKNOWN_REASON`      | 网络不可用或浏览器离线——这是意外中断最常见的原因 | 是        |
| 1  | `CLIENT_INITIATED`    | 用户调用了 `disconnect()`      | 否——这是有意的 |
| 2  | `DUPLICATE_IDENTITY`  | 具有相同身份的另一个会话已加入           | 否——提示用户  |
| 3  | `SERVER_SHUTDOWN`     | 服务器重启                     | 是——有延迟   |
| 4  | `PARTICIPANT_REMOVED` | 通过服务器 API 移除              | 否        |
| 5  | `ROOM_DELETED`        | 会话已在服务器端关闭                | 否        |
| 6  | `STATE_MISMATCH`      | 客户端/服务器状态出现分歧             | 是        |
| 7  | `JOIN_FAILURE`        | 加入失败——检查配置                | 否——修复配置  |
| 9  | `SIGNAL_CLOSE`        | WebSocket 信令通道已正常关闭       | 是        |

**`UNKNOWN_REASON` 与 `SIGNAL_CLOSE`:** 两者都表示网络中断，并且都应触发重试。区别在于时机——当浏览器完全离线时（`n +avigator.onLine = false`），LiveKit 会通过其离线检测器立即触发 `UNKNOWN_REASON (0)` ，甚至在 WebSocket 关闭之前。 `SIGNAL_CLOSE (9)` 会在信令 WebSocket 自身关闭时触发，这要求网络仍有部分可达性。实际上，拔掉 WiFi 或在 DevTools → Offline 下都会产生 `UNKNOWN_REASON`.

最后一个原因也可以在同步状态下查看： `client.state.disconnectReason` — `null` 在已连接时。

***

### `serverResponse` 事件

服务器会对 **你客户端发送的每条消息** — `sendUserTextMessage`, `updateContext`, `sendTriggerMessage`, `toggleTts`等发送确认。检查 `状态` 以了解服务器是否已接受该请求。

#### React

```tsx
import type { ServerResponse } from '@convai/web-sdk';
import { useEffect } from 'react';

useEffect(() => {
  return client.on('serverResponse', (res: ServerResponse) => {
    if (res.status === 'error') {
      console.error(`[${res.event_type}] failed: ${res.message}`);
    }

    // context-update 包含当前令牌预算
    if (res.event_type === 'context-update' && res.extras) {
      const { token_count, remaining_tokens, max_tokens } = res.extras;
      console.log(`上下文：已使用 ${token_count}/${max_tokens} 个 token，剩余 ${remaining_tokens} 个`);
      if (remaining_tokens < 5000) {
        console.warn('上下文预算不足——请考虑重置临时上下文');
      }
    }
  });
}, [client]);
```

#### 原生 JS

```ts
import type { ServerResponse } from '@convai/web-sdk/core';

client.on('serverResponse', (res: ServerResponse) => {
  if (res.status === 'error') {
    console.error(`[${res.event_type}] failed: ${res.message}`);
  }

  if (res.event_type === 'context-update' && res.extras) {
    const { token_count, remaining_tokens, max_tokens } = res.extras;
    console.log(`上下文：已使用 ${token_count}/${max_tokens} 个 token，剩余 ${remaining_tokens} 个`);
  }
});

```

#### 载荷结构

```ts
interface ServerResponse {
  event_type: string;                                        // 你发送的消息类型
  status: 'success' | 'error' | 'processing' | 'pending';
  message: string | null;                                    // 出错时非空
  extras: ServerResponseExtras | null;                       // 事件特定数据
}
```

对于 `context-update`, `extras` 包含令牌预算信息：

```ts
{
  token_count: number;       // 当前已使用的 token
  max_tokens: number;        // 预算上限（50 000）
  remaining_tokens: number;  // 在开始 LRU 裁剪前剩余的 token
}
```

***

### `llmNoResponse` 事件

在 LLM 明确选择不响应时触发——这不是错误，但你的 UI 应该停止显示“思考中”指示器。

```ts
client.on('llmNoResponse', () => {
  hideThinkingIndicator();
});
```

***

### `idleWarning` 事件

在服务器断开空闲会话之前触发。在任何用户活动时调用 `resetIdleTimer()` 以保持会话存活。

```ts
client.on('idleWarning', ({ remainingSeconds }) => {
  if (remainingSeconds !== null) {
    showBanner(`由于不活动，会话将在 ${remainingSeconds} 秒后关闭`);
  }
  // 在任何用户交互时重置
  document.addEventListener('click', () => client.resetIdleTimer(), { once: true });
});
```

***

### 可靠性模式

#### 使用指数退避重试

SDK 不会自动重连。请在非有意断开时实现你自己的退避重试。

```ts
async function retryWithBackoff(
  fn: () => Promise<void>,
  attempts = 3,
  delayMs = 500,
): Promise<void> {
  for (let i = 1; i <= attempts; i++) {
    try {
      return await fn();
    } catch (err) {
      if (i === attempts) throw err;
      await new Promise((r) => setTimeout(r, delayMs * 2 ** (i - 1)));
    }
  }
}

client.on('disconnect', (reason) => {
  if (
    reason !== DisconnectReasonEnum.CLIENT_INITIATED &&
    reason !== DisconnectReasonEnum.DUPLICATE_IDENTITY &&
    reason !== DisconnectReasonEnum.JOIN_FAILURE
  ) {
    retryWithBackoff(() => client.reconnect());
  }
});
```

#### 安全发送保护

发送前检查连接状态，以避免静默丢失。

```ts
function safeSend(client: IConvaiClient, text: string) {
  if (!client.state.isConnected || !client.isBotReady) return;
  client.sendUserTextMessage(text);
}
```

#### 保护媒体控制调用

音频和视频控制方法是异步的，并且在权限被拒绝时可能抛出错误。

```ts
async function safeToggleMic(client: IConvaiClient) {
  try {
    await client.audioControls.toggleAudio();
  } catch (err) {
    console.error('麦克风切换失败：', err);
    // 显示权限提示或设备错误消息
  }
}
```

#### 始终取消订阅

每个 `client.on(...)` 调用都会返回一个取消订阅函数。请在卸载或会话拆除时调用它。

```tsx
// React
useEffect(() => {
  const unsubs = [
    client.on('error', handleError),
    client.on('disconnect', handleDisconnect),
    client.on('serverResponse', handleServerResponse),
  ];
  return () => unsubs.forEach((u) => u());
}, [client]);
```

```ts
// 原生
const unsubError = client.on('error', handleError);
const unsubDisconnect = client.on('disconnect', handleDisconnect);

// 清理时
unsubError();
unsubDisconnect();
```

***

### 快速参考——应使用哪个通道

| 场景                     | 通道                                           |
| ---------------------- | -------------------------------------------- |
| 会话进行中网络中断              | `断开连接` → `SIGNAL_CLOSE` (9)                  |
| 用户按下断开连接               | `断开连接` → `CLIENT_INITIATED` (1)              |
| 同一用户打开了另一个标签页          | `断开连接` → `DUPLICATE_IDENTITY` (2)            |
| 服务器维护                  | `断开连接` → `SERVER_SHUTDOWN` (3)               |
| `updateContext` 被服务器拒绝 | `serverResponse` → `status: 'error'`         |
| 动态上下文 token 预算过低       | `serverResponse` → `extras.remaining_tokens` |
| WebRTC 协商失败            | `错误` — 代码 13                                 |
| 麦克风权限被拒绝               | `错误` — 代码 1（子原因 0）                           |
| LLM 决定不回复              | `llmNoResponse`                              |
| 会话即将超时                 | `idleWarning`                                |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.convai.com/api-docs/zh/cha-jian-yu-ji-cheng/web-plugins/convai-web-sdk/cuo-wu-chu-li.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
