> 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/convai-unity-sdk/scripting-reference/session-events.md).

# 会话事件

SDK 通过两种接线方式暴露会话级事件，你可以单独使用，也可以同时使用。 `ConvaiSessionEventRelay` 是一个 MonoBehaviour，可将事件连接到在 Inspector 中分配的 `UnityEvent` 回调，无需编写代码。 `ConvaiEvents` 是一个 C# 类型化事件中心，可通过以下方式从任何脚本访问： `ConvaiManager.ActiveManager.Events`。这两种方式触发的是同一底层 SDK 事件——请根据你的代码需求选择。有关中继组件的概念概览以及何时选择每种方式，请参见 [事件系统](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/core-concepts/event-system.md).

***

### Inspector 接线 — `ConvaiSessionEventRelay`

{% tabs %}
{% tab title="检视面板" %}
**设置：**

1. 将 `ConvaiSessionEventRelay` 通过以下方式添加到场景中的任意 GameObject： **Add Component → Convai → Events → Convai Session Event Relay**.
2. 分配一个 `ConvaiManager` 引用到 **Manager** 字段中，或者将其留空并启用 **Auto Resolve Manager** 以便在运行时使用 `ConvaiManager.ActiveManager` 。
3. 在 Inspector 中按每个事件接线你的回调。

{% hint style="warning" %}
**Auto Resolve Manager** 很方便，但不是确定性的。对于包含多个 `ConvaiManager` 实例的场景，请显式分配目标管理器。
{% endhint %}
{% endtab %}

{% tab title="脚本" %}

```csharp
using Convai.Runtime.Facades;
using UnityEngine;

public class SessionStatusMonitor : MonoBehaviour
{
    private void OnEnable()
    {
        var manager = ConvaiManager.ActiveManager;
        if (manager == null) return;

        manager.Events.OnConnected    += HandleConnected;
        manager.Events.OnDisconnected += HandleDisconnected;
        manager.Events.OnSessionError += HandleError;
    }

    private void OnDisable()
    {
        var manager = ConvaiManager.ActiveManager;
        if (manager == null) return;

        manager.Events.OnConnected    -= HandleConnected;
        manager.Events.OnDisconnected -= HandleDisconnected;
        manager.Events.OnSessionError -= HandleError;
    }

    private void HandleConnected()             => Debug.Log("会话已连接。");
    private void HandleDisconnected()          => Debug.Log("会话已断开。");
    private void HandleError(SessionError err) => Debug.LogError($"[{err.ErrorCode}] {err.Message}");
}
```

{% endtab %}
{% endtabs %}

***

### `ConvaiSessionEventRelay` 事件

| 事件                      | 参数                             | 触发时机                    |
| ----------------------- | ------------------------------ | ----------------------- |
| `OnConnected`           | —                              | 会话切换到 `已连接`             |
| `OnDisconnected`        | —                              | 会话切换到 `已断开连接`           |
| `OnReconnecting`        | —                              | 会话切换到 `重新连接中`           |
| `OnReconnected`         | —                              | 重连成功（从 `重新连接中` 为 `已连接`) |
| `OnUsageLimitReached`   | —                              | Convai 报告配额已耗尽          |
| `OnSessionStateChanged` | `SessionStateChangedRelayData` | 任意 `SessionState` 转换    |
| `OnSessionError`        | `SessionErrorRelayData`        | 会话遇到错误                  |

#### `SessionStateChangedRelayData` 字段

| 字段                         | 类型             | 描述                                      |
| -------------------------- | -------------- | --------------------------------------- |
| `OldState`                 | `SessionState` | 转换前的状态                                  |
| `NewState`                 | `SessionState` | 转换后的状态                                  |
| `SessionId`                | `string`       | 当前会话标识符                                 |
| `ErrorCode`                | `string`       | 如果新状态为则返回错误代码 `错误`；否则为空                 |
| `IsError`                  | `bool`         | 当以下情况时为 True `NewState == Error`        |
| `IsReconnecting`           | `bool`         | 当从以下状态转换时为 True `已连接` 为 `重新连接中`         |
| `IsConnectionEstablished`  | `bool`         | 当从以下状态转换时为 True `连接中` 为 `已连接`           |
| `IsReconnectionSuccessful` | `bool`         | 当从以下状态转换时为 True `重新连接中` 为 `已连接`         |
| `IsDisconnected`           | `bool`         | 当以下情况时为 True `NewState == Disconnected` |

#### `SessionErrorRelayData` 字段

| 字段                  | 类型                  | 描述                                |
| ------------------- | ------------------- | --------------------------------- |
| `ErrorCode`         | `string`            | 层级错误代码，例如 `"connection.timeout"`  |
| `Message`           | `string`            | 人类可读的错误描述                         |
| `SessionId`         | `string`            | 发生错误时的会话标识符                       |
| `IsRecoverable`     | `bool`              | 如果 SDK 可以尝试自动重连，则为 True           |
| `Stage`             | `SessionErrorStage` | 错误来源的宽泛生命周期阶段                     |
| `HttpStatusCode`    | `int`               | 如果错误来自 HTTP 响应，则为 HTTP 状态码；否则为 0  |
| `HasHttpStatusCode` | `bool`              | 当以下情况时为 True `HttpStatusCode > 0` |

***

### C# 事件中心 — `ConvaiEvents`

通过以下方式访问 `ConvaiManager.ActiveManager.Events`。在 `OnEnable`中订阅，在 `OnDisable`.

#### 会话作用域事件

| 事件                                  | 参数类型                              | 触发时机                 |
| ----------------------------------- | --------------------------------- | -------------------- |
| `OnConnected`                       | —                                 | 会话到达 `已连接`           |
| `OnDisconnected`                    | —                                 | 会话到达 `已断开连接`         |
| `OnSessionStateChanged`             | `SessionStateChanged`             | 任意 `SessionState` 转换 |
| `OnSessionError`                    | `SessionError`                    | 会话遇到错误               |
| `OnPipelineError`                   | `SessionError`                    | 处理管道遇到错误（与会话级错误不同）   |
| `OnUsageLimitReached`               | `UsageLimitReached`               | Convai 报告配额已耗尽       |
| `OnUserIdleWarningReceived`         | `UserIdleWarningReceived`         | Convai 警告会话将因不活动而关闭  |
| `OnParticipantJoined`               | `ParticipantInfo`                 | 一名参与者加入房间            |
| `OnParticipantLeft`                 | `ParticipantInfo`                 | 一名参与者离开房间            |
| `OnRoomOwnershipRebindStateChanged` | `RoomOwnershipRebindStateChanged` | 当前角色所有权重绑定发生状态变化     |

#### 领域事件载荷类型

**`SessionStateChanged`**

| 字段          | 类型              | 描述                               |
| ----------- | --------------- | -------------------------------- |
| `OldState`  | `SessionState`  | 转换前的状态                           |
| `NewState`  | `SessionState`  | 转换后的状态                           |
| `SessionId` | `string`        | 会话标识符                            |
| `时间戳`       | `DateTime`      | 转换发生的 UTC 时间                     |
| `错误`        | `SessionError?` | 当以下情况时存在 `NewState == Error`     |
| `ErrorCode` | `string`        | 快捷方式到 `Error?.ErrorCode`；没有错误时为空 |

**`UsageLimitReached`**

| 字段          | 类型         | 描述                               |
| ----------- | ---------- | -------------------------------- |
| `QuotaType` | `string`   | 耗尽的是哪种配额（例如 `"monthly_minutes"`) |
| `Message`   | `string`   | 来自 Convai 的人类可读描述                |
| `时间戳`       | `DateTime` | 事件发生的 UTC 时间                     |

**`UserIdleWarningReceived`**

| 字段                 | 类型         | 描述                     |
| ------------------ | ---------- | ---------------------- |
| `RemainingSeconds` | `int`      | Convai 因不活动而关闭会话前剩余的秒数 |
| `Message`          | `string`   | 人类可读的警告消息              |
| `时间戳`              | `DateTime` | 事件发生的 UTC 时间           |

**`ParticipantInfo`**

| 字段                | 类型                | 描述                    |
| ----------------- | ----------------- | --------------------- |
| `ParticipantId`   | `string`          | 参与者唯一标识符              |
| `Identity`        | `string`          | 与此参与者关联的 Identity 字符串 |
| `DisplayName`     | `string`          | 人类可读的显示名称             |
| `ParticipantType` | `ParticipantType` | 该参与者是本地玩家、远程玩家还是角色    |
| `IsLocal`         | `bool`            | 本地玩家参与者为 True         |
| `IsMuted`         | `bool`            | 当该参与者的音频被静音时为 True    |

**`RoomOwnershipRebindStateChanged`**

| 字段                     | 类型                          | 描述                  |
| ---------------------- | --------------------------- | ------------------- |
| `Outcome`              | `RoomOwnershipRebindStatus` | 重绑定尝试的结果            |
| `HasPendingReconnect`  | `bool`                      | 当需要重连才能完成重绑定时为 True |
| `SessionState`         | `SessionState`              | 事件发生时的会话状态          |
| `ActiveCharacterId`    | `string`                    | 当前绑定用于对话的角色 ID      |
| `RequestedCharacterId` | `string`                    | 请求用于重绑定的角色 ID       |
| `时间戳`                  | `DateTime`                  | 事件发生的 UTC 时间        |
| `ReconnectRequired`    | `bool`                      | 当所有权变更需要会话重连时为 True |

***

### 支持类型

#### `SessionState` 枚举

| 值                   | 描述                        |
| ------------------- | ------------------------- |
| `已断开连接` (0)         | 没有活动会话。初始状态，以及正常断开后的最终状态。 |
| `连接中` (1)           | 正在尝试建立新会话。                |
| `已连接` (2)           | 会话处于活动状态并接受输入。            |
| `重新连接中` (3)         | 连接丢失；SDK 正在尝试自动恢复。        |
| `Disconnecting` (4) | 正在优雅地关闭会话。                |
| `错误` (5)            | 不可恢复错误。若不显式重置，会话无法继续。     |

**扩展方法** （在 `SessionState`):

| 方法                  | 返回 `true` 当                          |
| ------------------- | ------------------------------------ |
| `IsConnected()`     | 状态为 `已连接` 或 `重新连接中`                  |
| `IsTransitioning()` | 状态为 `连接中`, `重新连接中`，或 `Disconnecting` |
| `IsStable()`        | 状态为 `已断开连接`, `已连接`，或 `错误`            |
| `CanAcceptInput()`  | 状态为 `已连接` 仅                          |

#### `SessionErrorStage` 枚举

| 值                     | 描述                      |
| --------------------- | ----------------------- |
| `Unknown` (0)         | 无法确定错误阶段                |
| `配置` (1)              | 读取或应用 SDK 配置时发生错误       |
| `ConnectApi` (2)      | API 连接设置期间发生错误          |
| `Transport` (3)       | 传输层中的错误（WebSocket、gRPC） |
| `SessionRecovery` (4) | 重连尝试期间发生错误              |
| `Runtime` (5)         | 活动会话期间发生错误              |

#### `ParticipantType` 枚举

| 值                  | 描述         |
| ------------------ | ---------- |
| `Unknown` (0)      | 未确定参与者类型   |
| `LocalPlayer` (1)  | 此客户端上的本地玩家 |
| `RemotePlayer` (2) | 多人会话中的远程玩家 |
| `Character` (3)    | 一个 AI 角色   |

#### `RoomOwnershipRebindStatus` 枚举

| 值                              | 描述                 |
| ------------------------------ | ------------------ |
| `DeferredUntilStartup` (0)     | 重绑定已排队；将在运行时启动后应用  |
| `AppliedImmediately` (1)       | 重绑定已应用，无需重连        |
| `PendingReconnect` (2)         | 重绑定已暂存；需要重连才能生效    |
| `RejectedTransitionState` (3)  | 被拒绝，因为会话当前正在转换     |
| `RejectedInvalidOwnership` (4) | 被拒绝，因为请求的角色不是有效所有者 |

#### `SessionError` 结构体

完整的错误详情类型，用作以下内容的参数： `OnSessionError` 和 `OnPipelineError` 在 C# 事件中心中，以及通过以下方式在 `SessionErrorRelayData` 中暴露。

| 成员                  | 类型                  | 描述                                                               |
| ------------------- | ------------------- | ---------------------------------------------------------------- |
| `ErrorCode`         | `string`            | 层级代码，例如 `"connection.timeout"`, `"transport.closed"`             |
| `Message`           | `string`            | 人类可读的描述                                                          |
| `SessionId`         | `string`            | 发生错误时的会话标识符                                                      |
| `时间戳`               | `DateTime`          | 错误发生的 UTC 时间                                                     |
| `IsRecoverable`     | `bool`              | 当 SDK 会尝试自动恢复时为 True                                             |
| `Exception`         | `Exception`         | 底层异常（如果有）                                                        |
| `Stage`             | `SessionErrorStage` | 错误来源的生命周期阶段                                                      |
| `HttpStatusCode`    | `int?`              | API 层错误的 HTTP 状态码                                                |
| `类别`                | `string`            | 的第一个片段 `ErrorCode` （例如 `"connection"` 来自 `"connection.timeout"`) |
| `IsConnectionError` | `bool`              | 当以下情况时为 True `ErrorCode` 以以下内容开头 `"connection."`                 |
| `IsSessionError`    | `bool`              | 当以下情况时为 True `ErrorCode` 以以下内容开头 `"session."`                    |
| `IsTransportError`  | `bool`              | 当以下情况时为 True `ErrorCode` 以以下内容开头 `"transport."`                  |
| `IsProtocolError`   | `bool`              | 当以下情况时为 True `ErrorCode` 以以下内容开头 `"protocol."`                   |
| `IsServerError`     | `bool`              | 当以下情况时为 True `ErrorCode` 以以下内容开头 `"server."`                     |

***

### 高级 — `ConvaiEvents.Raw` 事件中心

{% hint style="warning" %}
这是一个高级模式。对于所有常见场景，请使用 `ConvaiEvents` 上的类型化属性。
{% endhint %}

`ConvaiEvents.Raw` 暴露了底层的 `IEventHub`，这使你可以订阅任何在 `ConvaiEvents`.

上没有对应类型化属性的领域事件类型。 **原始事件中心使用基于令牌的订阅模型。** 丢失令牌会导致内存泄漏

```csharp
— 事件中心会持有适配器的引用，直到显式取消订阅。
using Convai.Domain.DomainEvents.Session;
using UnityEngine;

public class RawHubExample : MonoBehaviour
{
    private SubscriptionToken _token;

    private void OnEnable()
    {
        var hub = ConvaiManager.ActiveManager?.Events?.Raw;
        if (hub == null) return;

        _token = hub.Subscribe<SessionStateChanged>(
            e => Debug.Log($"原始事件中心：{e.OldState} → {e.NewState}"),
            EventDeliveryPolicy.MainThread);
    }

    private void OnDisable()
    {
        ConvaiManager.ActiveManager?.Events?.Raw?.Unsubscribe(_token);
        _token = default;
    }
}
```

**`EventDeliveryPolicy` 选项：**

| 值                 | 适用场景                                    |
| ----------------- | --------------------------------------- |
| `MainThread` （默认） | 所有 Unity API 访问——在下一个 Unity `Update` 刻度 |
| `Background`      | 非 Unity 线程安全的日志记录或指标统计                  |
| `Immediate`       | 在发布线程上同步传递（谨慎使用）                        |

***

### 使用示例

#### 示例 1 — 在断开连接时变暗的监考 HUD

一个培训模拟展示了一个监考覆盖层：当会话中断时它会变暗，从而为学习者提供清晰的视觉信号，表明 AI 角色当前不可用。

```csharp
— 事件中心会持有适配器的引用，直到显式取消订阅。
using Convai.Runtime.Facades;
using UnityEngine;

public class ProctorHUD : MonoBehaviour
{
    [SerializeField] private CanvasGroup _overlay;

    private void OnEnable()
    {
        var mgr = ConvaiManager.ActiveManager;
        if (mgr == null) return;
        mgr.Events.OnConnected           += OnConnected;
        mgr.Events.OnDisconnected        += OnDisconnected;
        mgr.Events.OnSessionStateChanged += OnSessionStateChanged;
    }

    private void OnDisable()
    {
        var mgr = ConvaiManager.ActiveManager;
        if (mgr == null) return;
        mgr.Events.OnConnected           -= OnConnected;
        mgr.Events.OnDisconnected        -= OnDisconnected;
        mgr.Events.OnSessionStateChanged -= OnSessionStateChanged;
    }

    private void OnConnected()    => SetAlpha(1f);
    private void OnDisconnected() => SetAlpha(0.3f);

    private void OnSessionStateChanged(SessionStateChanged e)
    {
        if (e.NewState == SessionState.Reconnecting)
            SetAlpha(0.6f);
    }

    private void SetAlpha(float a) => _overlay.alpha = a;
}
```

#### 示例 2 — 带可恢复分支的会话错误横幅

一个企业入职模拟显示可关闭的错误横幅。可恢复错误会显示“正在重连…”消息；不可恢复错误则提示学员重新加载模块。

```csharp
— 事件中心会持有适配器的引用，直到显式取消订阅。
using Convai.Runtime.Facades;
using TMPro;
using UnityEngine;

public class ErrorBanner : MonoBehaviour
{
    [SerializeField] private GameObject _bannerRoot;
    [SerializeField] private TMP_Text   _messageLabel;
    [SerializeField] private GameObject _reloadButton;

    private void OnEnable()  => ConvaiManager.ActiveManager?.Events.OnSessionError += ShowError;
    private void OnDisable() => ConvaiManager.ActiveManager?.Events.OnSessionError -= ShowError;

    private void ShowError(SessionError err)
    {
        _bannerRoot.SetActive(true);
        _messageLabel.text = err.IsRecoverable
            ? "连接中断。正在重连…"
            : $"会话错误：{err.Message}。请重新加载模块。";
        _reloadButton.SetActive(!err.IsRecoverable);
    }
}
```

#### 示例 3 — 空闲警告倒计时器

一个医疗培训模拟在 Convai 警告会话即将关闭时显示倒计时，为学习者提供在 AI 角色断开前恢复的时间。

```csharp
— 事件中心会持有适配器的引用，直到显式取消订阅。
using Convai.Runtime.Facades;
using System.Collections;
using TMPro;
using UnityEngine;

public class IdleCountdown : MonoBehaviour
{
    [SerializeField] private TMP_Text _countdownLabel;

    private Coroutine _countdown;

    private void OnEnable()  => ConvaiManager.ActiveManager?.Events.OnUserIdleWarningReceived += StartCountdown;
    private void OnDisable() => ConvaiManager.ActiveManager?.Events.OnUserIdleWarningReceived -= StartCountdown;

    private void StartCountdown(UserIdleWarningReceived e)
    {
        if (_countdown != null) StopCoroutine(_countdown);
        _countdown = StartCoroutine(CountdownRoutine(e.RemainingSeconds));
    }

    private IEnumerator CountdownRoutine(int seconds)
    {
        while (seconds > 0)
        {
            _countdownLabel.text = $"会话将在 {seconds} 秒后关闭";
            yield return new WaitForSeconds(1f);
            seconds--;
        }
        _countdownLabel.text = string.Empty;
    }
}
```

***

### 故障排查

| 症状                                              | 可能原因                                   | 修复方法                                                         |
| ----------------------------------------------- | -------------------------------------- | ------------------------------------------------------------ |
| `OnSessionError` 从不触发                           | 在连接尝试之后订阅；错过了该事件                       | 在……中订阅 `OnEnable`，确保 `OnEnable` 在……之前运行 `ConnectAsync`       |
| `ConvaiSessionEventRelay` 未调用回调                 | 未分配 Manager 且 `AutoResolveManager` 已关闭 | 启用 **Auto Resolve Manager** 或分配 `ConvaiManager` 字段           |
| 会话卡在 `重新连接中` 中，且一直持续                            | 服务器不可达或已达到重试上限                         | 检查网络；监听 `OnSessionError` 与 `IsRecoverable == false` 以检测终态失败  |
| `OnConnected` 触发但 `CanAcceptInput()` 返回 `false` | 状态检查在转换期间被调用                           | 评估 `CanAcceptInput()` 在……内部 `OnConnected` 处理程序，而不是在 `Update` |

***

### 下一步

随着会话事件已接好，转到 [角色事件](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/scripting-reference/character-events.md) 以响应语音、情绪和轮次生命周期。关于脚本中的连接控制，请参见 [ConvaiManager API](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/scripting-reference/convaimanager-api.md).


---

# 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/convai-unity-sdk/scripting-reference/session-events.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.
