> 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
    [*] --> 已断开连接

    已断开连接 --> 正在连接 : 已发起连接
    正在连接 --> 已连接 : 连接已建立
    正在连接 --> 错误 : 配置或身份验证失败

    已连接 --> 正在断开连接 : 已发起断开连接
    正在断开连接 --> 已断开连接 : 正常关闭

    已连接 --> 正在重新连接 : 连接丢失
    正在重新连接 --> 已连接 : 重新连接成功
    正在重新连接 --> 错误 : 已超过最大尝试次数
```

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

你会以以下形式收到状态转换： `SessionStateChangedRelayData` 事件，通过 `ConvaiSessionEventRelay`。请参阅 [事件系统](/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`                | 在会话进入以下状态之前的最大自动重连尝试次数： `错误` 状态时。                 |
| `SpawnAgentOnRejoin`       | `bool`         | `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` 已经处于 `连接中` 或 `正在断开连接` 时调用， `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);
```

| 值                 | 角色音频                                  | 规范转写历史   | 已下发转写展示         | LipSync                                   |
| ----------------- | ------------------------------------- | -------- | --------------- | ----------------------------------------- |
| `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($"Background policy fell back from {data.RequestedPolicy} to {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`    |
| 会话卡在 `连接中` 一直                                      | `StartWaitTimeoutMs` 未针对慢速网络进行配置；或者防火墙阻止了传输               | 增加 `StartWaitTimeoutMs` 在 `ReconnectPolicy`；请检查到 Convai 端点的网络访问                    |
| 重连循环始终不成功；会话最终达到 `错误`                              | `MaxReconnectAttempts` 耗尽                                 | 订阅 `ConvaiSessionEventRelay.OnSessionStateChanged` 并将错误呈现给用户；在用户确认后手动调用重新连接        |
| 两个角色共享同一个会话 ID                                     | Inspector 中的角色 ID 字符串相同                                   | 为每个角色分配唯一的角色 ID `ConvaiCharacter` 在场景中                                             |
| `ResumeAsync()` 不会恢复音频                             | 应用仍在后台，因此后台策略会保持手动暂停原因处于激活状态                              | 等待应用返回前台，或者检查 `IsBackgrounded` 在最新的 `OnRuntimeBackgroundStateChanged` payload      |
| `ReconnectAsync()` 会抛出 `InvalidOperationException` | 在以下情况下调用： `SessionState` 已经 `连接中` 或 `正在断开连接`              | 检查 `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 by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

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

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

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.
