> 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/core-concepts/session-lifecycle.md).

# 会话生命周期

每个 `ConvaiCharacter` 场景中的每个角色都会与 Convai 保持一个独立会话。该会话会跟踪角色是否已连接、当前状态是什么，以及——在启用持久化时——你上次连接时它处于哪段对话中。理解会话如何创建、持久化和恢复，能让你在训练模拟、交互体验和游戏中构建可靠、可恢复的角色交互。

***

### 会话状态机

每个角色会话都会经历以下状态。

```mermaid
stateDiagram-v2
    [*] --> Disconnected

    Disconnected --> Connecting : 开始连接
    Connecting --> Connected : 连接已建立
    Connecting --> Error : 配置或认证失败

    Connected --> Disconnecting : 开始断开连接
    Disconnecting --> Disconnected : 正常关闭

    Connected --> Reconnecting : 连接丢失
    Reconnecting --> Connected : 重新连接成功
    Reconnecting --> Error : 超过最大尝试次数
```

| 状态              | 值 | 含义                                       |
| --------------- | - | ---------------------------------------- |
| `Disconnected`  | 0 | 没有活动会话。初始状态，以及正常断开后的最终状态。                |
| `Connecting`    | 1 | 连接尝试进行中。正在从 Disconnected 过渡到 Connected。  |
| `Connected`     | 2 | 会话处于活动状态。音频流和对话正在进行。                     |
| `Reconnecting`  | 3 | 连接已丢失。SDK 正在尝试自动重新建立连接。                  |
| `Disconnecting` | 4 | 正在进行优雅关闭。正在从 Connected 过渡到 Disconnected。 |
| `错误`            | 5 | 不可恢复的错误。需要手动干预才能重新连接。                    |

你会通过 `SessionStateChangedRelayData` 事件接收状态转换， `ConvaiSessionEventRelay`的 Unity 场景。参见 [事件系统](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/core-concepts/event-system.md) 了解如何订阅。

***

### 按角色划分的会话

每个 `ConvaiCharacter` 都有自己独立的会话。会话不会在角色之间共享。在多角色场景中，每个角色都会独立连接和断开——会话 ID 以 Inspector 中为角色设置的 ID 字符串为键（而不是场景或对象名称），重连策略按角色生效，而且一个角色的会话错误不会影响其他角色。

这描述的是每个 `ConvaiCharacter` 在本地跟踪的连接。当一个场景注册了两个或更多角色时，SDK 会在连接时围绕它们构建一个共享的多角色房间，并在这些按角色划分的会话之上叠加自己的成员与就绪模型。请参见 [多角色会话的工作方式](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/multi-character-sessions/how-multi-character-sessions-work.md) 了解该层。

`ConvaiSessionData` 是持久化会话存储，用于将每个角色映射到其当前会话标识。它会在启动时自动从磁盘加载，并在每次变更时写入 `{Application.persistentDataPath}/Convai/sessions.json` ——无需任何额外设置，会话 ID 即可在应用重启后保留。

| 方法                                       | 描述                              |
| ---------------------------------------- | ------------------------------- |
| `GetSessionId(characterId)`              | 返回该角色当前的会话 ID；如果不存在则返回 `null` 。 |
| `StoreSessionId(characterId, sessionId)` | 为该角色存储一个会话 ID，并立即保存到磁盘。         |
| `ClearSessionId(characterId)`            | 移除某个角色的会话 ID 并保存。               |
| `ClearAllSessionIds()`                   | 移除所有已存储的会话 ID 并保存。              |
| `GetAllSessionIds()`                     | 返回当前所有“角色→sessionId”映射的只读快照。    |

{% hint style="info" %}
`ConvaiSessionData` 是单例。数据存储在 `{Application.persistentDataPath}/Convai/sessions.json` ，并会在应用重启之间持续保留。若需要清空初始状态，请显式调用 `ClearAllSessionIds()` 。
{% endhint %}

***

### 会话持久化

当会话 ID 被持久化后，SDK 可在下次连接时恢复之前的对话——角色会记住先前交互的上下文。

#### 哪些会保留，哪些会重置

| 重新连接时      | 行为                                   |
| ---------- | ------------------------------------ |
| 会话 ID      | 通过以下方式持久化 `ConvaiSessionData` ——支持恢复 |
| 对话历史       | 由 Convai 管理；当会话 ID 有效时会恢复            |
| 进行中的音频     | 重置——任何中途的音频都会被丢弃                     |
| 当前轮次状态     | 重置——轮次会干净地重新开始                       |
| 模块状态（例如情绪） | 重置——模块会在重新连接时重新初始化                   |

#### 默认持久化栈

SDK 通过 `ISessionPersistence` 提供可插拔的持久化层，供需要自定义后端存储（加密存储、云存档、数据库）的项目使用。默认栈如下：

```
ISessionPersistence
  └─ KeyValueStoreSessionPersistence        ← 使用前缀 "convai.session." 将 characterId → sessionId 映射
       └─ PlayerPrefsKeyValueStore           ← 默认的 IKeyValueStore 实现；封装 Unity PlayerPrefs
            └─ UnityEngine.PlayerPrefs       ← 持久化到磁盘
```

会话 ID 以如下格式的键存储： `convai.session.<characterId>`.

#### 替换持久化存储

实现 `IKeyValueStore` 可用于任何后端存储——数据库、加密存储、云存档系统。 `PlayerPrefsKeyValueStore` 会在内部将所有读写都转发到 Unity 主线程；如果你的后端存储有线程限制，请采用相同的线程安全模式。

```csharp
public sealed class SecureKeyValueStore : IKeyValueStore
{
    public string GetString(string key, string defaultValue = null)
    {
        return SecureStorage.GetValue(key) ?? defaultValue;
    }

    public void SetString(string key, string value)
    {
        SecureStorage.SetValue(key, value);
    }

    public bool HasKey(string key) => SecureStorage.HasKey(key);

    public void DeleteKey(string key) => SecureStorage.DeleteKey(key);

    public void Save() => SecureStorage.Flush();
}
```

通过以下方式注册： `ConvaiRuntimeBuilder`:

```csharp
var runtime = new ConvaiRuntimeBuilder()
    .UsePersistence(new MyPersistenceProvider(new SecureKeyValueStore()))
    .Build();
```

***

### 重连策略

`ReconnectPolicy` 控制当连接意外断开时 SDK 的行为。

| 字段                         | 类型             | 默认值                | 描述                                                |
| -------------------------- | -------------- | ------------------ | ------------------------------------------------- |
| `RoomRejoinTtlSeconds`     | `double`       | `60`               | 以秒为单位的窗口，在此期间 SDK 可在断线后重新加入现有房间。超过该窗口后，将改为创建新房间。  |
| `ResumePolicy`             | `ResumePolicy` | `ResumeIfPossible` | 控制 SDK 是否尝试通过以下方式恢复先前的对话： `character_session_id`. |
| `MaxReconnectAttempts`     | `int`          | `3`                | 在会话进入之前，自动重连的最大尝试次数 `错误` 状态时为 True。               |
| `SpawnAgentOnRejoin`       | `布尔值`          | `true`             | 重新加入现有房间时是否重新生成 AI 代理。                            |
| `StartWaitTimeoutMs`       | `int`          | `5000`             | 连接阶段的超时（毫秒），超过后该尝试被视为失败。 `Start()` 阶段             |
| `AutoMicStartDelaySeconds` | `float`        | `0.5`              | 连接后等待多少秒再启动麦克风。可防止在会话尚未完全就绪前采集音频。                 |

#### `ResumePolicy` 选项

| 值                  | 行为                              |
| ------------------ | ------------------------------- |
| `AlwaysFresh`      | 始终开始新的对话。角色不会记得之前的会话。           |
| `ResumeIfPossible` | 尝试恢复之前的对话。如果会话已过期或恢复失败，则回退到新会话。 |
| `AlwaysResume`     | 始终恢复。如果恢复失败，则连接失败——不会回退到新会话。    |

#### 预设策略

| 预设                                | 描述                                             |
| --------------------------------- | ---------------------------------------------- |
| `ReconnectPolicy.Default`         | 60 秒 TTL， `ResumeIfPossible`，3 次尝试，麦克风延迟 0.5 秒 |
| `ReconnectPolicy.AlwaysCreateNew` | 不尝试重新加入。始终创建新房间和新会话。                           |

```csharp
var policy = new ReconnectPolicy(
    roomRejoinTtlSeconds: 120,
    resumePolicy: ResumePolicy.AlwaysFresh,
    maxReconnectAttempts: 5,
    autoMicStartDelaySeconds: 1.0f
);
```

{% hint style="warning" %}
`AlwaysResume` 会将会话置于 `错误` 状态，如果 Convai 无法恢复会话（例如会话已过期）。除非你的训练模拟要求严格连续性，并且你已显式处理错误状态，否则请使用 `ResumeIfPossible` 。
{% endhint %}

***

### 显式会话控制

`ConvaiManager` 提供三个用于在自动重连之外控制会话的异步方法： `PauseAsync()`, `ResumeAsync()`，以及 `ReconnectAsync()`。当你的应用需要在不中断连接的情况下暂停演示，或按需强制建立新连接时，请使用它们——例如，训练模拟中的讲师休息暂停，或中断播放的菜单界面。

| 方法                 | 作用                                                                                                                                          |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `PauseAsync()`     | 在保持房间连接的同时，暂停 Convai 角色音频、已发送的转录展示以及运行时模块。                                                                                                  |
| `ResumeAsync()`    | 仅移除由以下方法设置的手动暂停原因： `PauseAsync()`。同时存在的应用后台暂停（见下文）会一直保持激活，直到 Unity 报告前台状态。                                                                  |
| `ReconnectAsync()` | 执行一次显式断开/连接循环，并使用上文描述的 `ReconnectPolicy` 和会话恢复行为。调用它时如果 `SessionState` 已经 `Connecting` 或 `Disconnecting` 将抛出一个 `InvalidOperationException`. |

```csharp
await manager.PauseAsync();
// ... 讲师休息 ...
await manager.ResumeAsync();
```

***

### 应用后台策略

`RuntimeBackgroundPolicy` 控制 Unity 应用处于后台时哪些内容继续运行——例如，玩家切换到其他标签页，或移动应用进入后台。可在以下位置设置项目默认值： **Edit > Project Settings > Convai SDK > Runtime Defaults > Background Policy**，或在运行时更改当前管理器：

```csharp
await manager.SetBackgroundPolicyAsync(RuntimeBackgroundPolicy.MuteButCatchUp);
```

| 值                 | 角色音频                               | 规范化转录历史  | 已发送转录展示          | 唇形同步                                      |
| ----------------- | ---------------------------------- | -------- | ---------------- | ----------------------------------------- |
| `ContinueAudibly` | 继续；请求后台执行                          | 继续       | 继续               | 继续按实时音频时钟推进                               |
| `PauseTimeline`   | Convai `AudioSource` 暂停；不影响无关的游戏音频 | 继续摄入房间事件 | 暂停时隐藏，恢复时再播放当前状态 | 展示节拍暂停；输入保持受限，恢复时会重新锚定到可用音频，因此可跳过已过期的缓冲数据 |
| `MuteButCatchUp`  | 本地静音，而播放继续推进                       | 继续       | 继续               | 继续；返回前台后按实时而非重放错过的展示恢复                    |

`ContinueAudibly` 和 `MuteButCatchUp` 设置 `Application.runInBackground` 时处于活动状态，但操作系统或浏览器仍可能挂起或静音该进程。WebGL 音频由浏览器路由，而不是由 Unity 所拥有，因此 `AudioSource`，所以 `PauseTimeline` 在该平台上会回退到 `MuteButCatchUp` ——状态变更事件会报告请求值和实际生效值。 `PauseTimeline` 只会暂停本地展示；它不会阻止 Convai 生成回复，也不会阻止 SDK 摄入转录内容。

订阅 `OnRuntimeBackgroundStateChanged` 来观察请求值和实际策略，因为当平台无法实现所请求的行为时，两者可能不同。管理器会在以下两种情况下应用所选策略： `OnApplicationPause` 以及失焦转换时应用，并且当 Unity 同时报告两个回调时不会重复暂停。

```csharp
[SerializeField] private ConvaiSessionEventRelay _relay;

private void OnEnable() =>
    _relay.OnRuntimeBackgroundStateChanged.AddListener(HandleBackgroundStateChanged);

private void HandleBackgroundStateChanged(RuntimeBackgroundStateRelayData data)
{
    if (data.RequestedPolicy != data.EffectivePolicy)
        Debug.LogWarning($"背景策略从 {data.RequestedPolicy} 回退为 {data.EffectivePolicy}。");
}
```

***

### 空闲警告和超时

Convai 会发送一条 `user-idle-warning` 消息，包含 `remaining_seconds` ，然后才断开空闲会话。SDK 将其暴露为 `ConvaiManager.Events.OnUserIdleWarningReceived` ，而对于基于 Inspector 的监听器，则为 `ConvaiSessionEventRelay.OnUserIdleWarning`.

管理器还会从该倒计时派生一个一次性的本地截止时间，并将其暴露为 `ConvaiManager.Events.OnUserIdleTimeoutElapsed` 和 `ConvaiSessionEventRelay.OnUserIdleTimeout`. `OnUserIdleTimeoutElapsed` 是客户端侧的截止时间信号，用于超时 UI 和恢复流程——它并不能确认 Convai 已关闭房间，因为 Convai 目前不会发送单独的超时包。请使用 `OnSessionStateChanged` 或 `OnDisconnected` 作为权威的传输状态。

语音、文本、触发器以及动态上下文活动都会重置 Convai 的空闲跟踪。对于警告后的仅 UI 活动，请调用 `ResetIdleTimer()` （或其面向 UI 的别名 `ExtendIdleTimeout()`）将截止时间向后推；当没有可接受重置的已连接房间时，这两个方法都返回 `false` 。

```csharp
private void HandleIdleWarning(UserIdleWarningRelayData warning)
{
    ShowIdlePrompt(warning.RemainingSeconds);
}

public void ContinueSession()
{
    if (!manager.ExtendIdleTimeout())
        ShowReconnectPrompt();
}
```

***

### 使用示例

#### 示例 1：医疗训练模拟——网络中断后恢复

学习者正在评估中途时网络断开。连接恢复后，患者角色会继续同一段对话——不会丢失上下文。

```csharp
var policy = new ReconnectPolicy(
    roomRejoinTtlSeconds: 120,          // 重新加入现有房间的 2 分钟窗口
    resumePolicy: ResumePolicy.ResumeIfPossible,
    maxReconnectAttempts: 5,
    autoMicStartDelaySeconds: 1.0f      // 为较慢的移动网络增加额外延迟
);
```

**预期结果：** SDK 会在 2 分钟窗口内自动重试最多 5 次。如果 Convai 端的会话仍然有效，对话就会从中断处继续。如果会话已过期，角色将开始一段新对话，而不是阻塞。

***

#### 示例 2：企业入职终端——始终全新的对话

每位接近终端的新员工都应从头开始，不记得之前的用户。 `AlwaysFresh` 和 `AlwaysCreateNew` 确保每次都是干净的初始状态。

```csharp
// 在 ConvaiRoomManager 的重连设置中应用该策略
var policy = ReconnectPolicy.AlwaysCreateNew;
// 在 AlwaysCreateNew 中，ResumePolicy 默认是 AlwaysFresh —— 不会沿用之前的会话
```

为确保在下一次会话开始前移除上一个用户的数据：

```csharp
public class KioskSessionReset : MonoBehaviour
{
    [SerializeField] private string _characterId;

    public void OnUserLogOut()
    {
        ConvaiSessionData.Instance.ClearSessionId(_characterId);
    }
}
```

**预期结果：** 每位新用户都会开始一段完全全新的对话。角色不会记得之前的交互，这对于共享终端部署来说是正确的。

***

#### 示例 3：在训练模拟中处理错误状态

当所有重连尝试都用尽后，会话进入 `错误` 状态。请将其展示给讲师，并允许手动重试，而不是静默卡住。

```csharp
public class SessionErrorHandler : MonoBehaviour
{
    [SerializeField] private ConvaiSessionEventRelay _relay;
    [SerializeField] private GameObject _errorPanel;
    [SerializeField] private ConvaiManager _manager;

    private void OnEnable()  => _relay.OnSessionStateChanged.AddListener(HandleStateChange);
    private void OnDisable() => _relay.OnSessionStateChanged.RemoveListener(HandleStateChange);

    private void HandleStateChange(SessionStateChangedRelayData data)
    {
        _errorPanel.SetActive(data.IsError);
    }

    // 由讲师的“重试”按钮调用
    public async void RetryConnection()
    {
        _errorPanel.SetActive(false);
        await _manager.ConnectAsync();
    }
}
```

**预期结果：** 当会话进入 `错误` 状态时会显示错误面板。讲师点击“重试”即可尝试重新建立连接，而无需重启模拟。

***

### 故障排查

| 症状                                                | 可能原因                                                                        | 修复方法                                                                                |
| ------------------------------------------------- | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| 会话停留在 `错误` 状态，断开后                                 | `AlwaysResume` 无法恢复会话，因为它已经过期                                               | 切换到 `ResumeIfPossible`；调用 `ClearSessionId(characterId)` 以移除过期的会话 ID，然后重新连接          |
| 尽管 `ResumeIfPossible`                             | 之前的 `ClearAllSessionIds()` 调用清除了会话文件，或者角色 ID 在运行之间发生了变化，角色在每次启动时仍会开始一段全新的对话 | 请验证 `characterId` 字符串在各次运行中保持稳定；检查 `{persistentDataPath}/Convai/sessions.json`      |
| 会话卡在 `Connecting` 中                               | `StartWaitTimeoutMs` 未针对慢速网络进行配置；或防火墙阻止了传输                                  | 增加 `StartWaitTimeoutMs` 位于 `ReconnectPolicy`；请验证对 Convai 端点的网络访问                    |
| 重连循环始终不成功；会话最终达到 `错误`                             | `MaxReconnectAttempts` 耗尽                                                   | 订阅 `ConvaiSessionEventRelay.OnSessionStateChanged` 并将错误展示给用户；在用户确认后手动调用重连           |
| 两个角色共享了同一个会话 ID                                   | Inspector 中的角色 ID 字符串相同                                                     | 为每个角色分配唯一的角色 ID `ConvaiCharacter` 即可                                                |
| `ResumeAsync()` 未恢复音频                             | 应用仍处于后台，因此后台策略会保持手动暂停原因处于激活状态                                               | 等待应用返回前台，或检查 `是否处于后台` 在最新的 `OnRuntimeBackgroundStateChanged` 载荷上                    |
| `ReconnectAsync()` 抛出 `InvalidOperationException` | 在以下状态下调用 `SessionState` 已经是 `Connecting` 或 `Disconnecting`                  | 检查 `SessionState` 在调用前，或等待进行中的转换完成                                                  |
| 空闲警告从未触发                                          | 没有订阅监听器，或者活动不断重置 Convai 的空闲跟踪                                               | 订阅 `OnUserIdleWarningReceived` 或 `OnUserIdleWarning`；请注意，语音、文本、触发器以及动态上下文活动都会重置空闲跟踪 |

***

### 下一步

你现在已经了解角色会话如何创建、状态转换如何工作、会话 ID 如何在重启间持久化，以及如何配置重连行为。接下来请阅读“轮换模式”，以配置 SDK 如何检测语音输入，然后阅读“事件系统”，以了解如何在场景脚本中订阅会话和角色事件。

{% content-ref url="/pages/251e0bf7030a5f742a1182e15529c0604e3ee150" %}
[轮替模式](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/core-concepts/turn-taking-modes.md)
{% endcontent-ref %}

{% content-ref url="/pages/0bd691fc4d8a06b0dbafd0f28b11f39be6f57f9a" %}
[事件系统](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/core-concepts/event-system.md)
{% endcontent-ref %}


---

# 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, and the optional `goal` query parameter:

```
GET https://docs.convai.com/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/core-concepts/session-lifecycle.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
