> 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 : connect initiated
    Connecting --> Connected : connection established
    Connecting --> Error : configuration or auth failure

    Connected --> Disconnecting : disconnect initiated
    Disconnecting --> Disconnected : clean shutdown

    Connected --> Reconnecting : connection lost
    Reconnecting --> Connected : reconnect succeeded
    Reconnecting --> Error : max attempts exceeded
```

| 状态           | 值 | 含义                                       |
| ------------ | - | ---------------------------------------- |
| `已断开连接`      | 0 | 没有活动会话。干净断开连接后的初始状态和最终状态。                |
| `Connecting` | 1 | 连接尝试进行中。正在从 Disconnected 转换到 Connected。  |
| `已连接`        | 2 | 会话处于活动状态。音频流和对话都在运行。                     |
| `重新连接中`      | 3 | 连接已丢失。SDK 正在尝试自动重新建立连接。                  |
| `断开连接中`      | 4 | 正在进行平滑关闭。正在从 Connected 转换到 Disconnected。 |
| `错误`         | 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`     | `整数`           | `3`                | 会话在进入之前允许的自动重连最大次数 `错误` 状态时为 True。        |
| `SpawnAgentOnRejoin`       | `布尔值`          | `是`                | 重新加入现有房间时是否重新生成 AI 代理。                    |
| `StartWaitTimeoutMs`       | `整数`           | `5000`             | 连接 `Start()` 阶段的超时时间（毫秒），超过后该次尝试将被视为失败。   |
| `AutoMicStartDelaySeconds` | `float`        | `0.5`              | 连接后等待多少秒再启动麦克风。可防止在会话完全就绪前捕获音频。           |

#### `ResumePolicy` options

| 值                  | 行为                                 |
| ------------------ | ---------------------------------- |
| `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` 或 `断开连接中` 期间调用它，会抛出一个 `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()`）来延后截止时间；当没有可接受重置的已连接房间时，两者都会返回 `否` 。

```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：企业入职 kiosk——始终全新的对话

每位来到 kiosk 的新员工都应该从头开始，并且不记得之前的用户。 `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);
    }
}
```

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

***

#### 示例 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()` 不会恢复音频                             | 应用仍处于后台，因此后台策略会保持手动暂停原因处于激活状态                            | 等待应用返回前台，或检查 `IsBackgrounded` 在最新的 `OnRuntimeBackgroundStateChanged` 有效载荷          |
| `ReconnectAsync()` 会抛出 `InvalidOperationException` | 在 `SessionState` 期间调用 `Connecting` 或 `断开连接中`             | 检查 `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.
