> 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/advanced-topics/custom-providers/custom-persistence-provider.md).

# 自定义持久化提供程序

Convai Unity SDK 将会话数据——连接状态、会话 ID、恢复令牌以及编辑器端最终用户 GUID——存储在 `PlayerPrefs` 默认通过 `PlayerPrefsKeyValueStore`。如果 `PlayerPrefs` 如果这适合您的部署，您就不需要本页。若您需要云端保存、加密存储、服务端会话管理，或用于自动化测试和 CI 的隔离存储，请替换持久化提供器。

### SDK 写入存储的内容

| 键前缀                  | 内容                | 重要性                                                      |
| -------------------- | ----------------- | -------------------------------------------------------- |
| `convai.session.*`   | 会话 ID 和恢复令牌       | 允许 SDK 在不重启 AI 回合的情况下恢复断开的会话                             |
| `convai.end_user_id` | 仅限编辑器的设备 GUID（回退） | 由……使用 `DeviceEndUserIdProvider` 在 Unity 编辑器中，当硬件 ID 不可用时 |
| `convai.prefs.*`     | 用户偏好（例如静音状态）      | 在应用启动之间持久化 SDK 级设置                                       |

替换持久化提供器也就替换了这些内容的读写位置。你的实现必须处理 SDK 涉及的每个键——下面的适配器模式可确保不会遗漏任何内容。

### 持久化接口

#### IKeyValueStore——简单存储

```csharp
namespace Convai.Domain.Abstractions
{
    public interface IKeyValueStore
    {
        string GetString(string key, string defaultValue = null);
        void   SetString(string key, string value);
        bool   HasKey(string key);
        void   DeleteKey(string key);
        void   Save();
    }
}
```

`Save()` 在写入操作后调用。对于内存存储而言它是无操作；对于基于文件的存储，它会刷新到磁盘。请在不需要异步操作的本地存储场景中实现此接口。

#### IPersistenceProvider——完整功能存储

```csharp
namespace Convai.Runtime.Core.Providers
{
    public interface IPersistenceProvider
    {
        // 同步读取
        string GetString(string key, string defaultValue = null);
        int    GetInt(string key, int defaultValue = 0);
        float  GetFloat(string key, float defaultValue = 0f);
        bool   GetBool(string key, bool defaultValue = false);
        bool   HasKey(string key);

        // 同步写入
        PersistenceResult SetString(string key, string value, PersistenceOptions options = default);
        PersistenceResult SetInt(string key, int value, PersistenceOptions options = default);
        PersistenceResult SetFloat(string key, float value, PersistenceOptions options = default);
        PersistenceResult SetBool(string key, bool value, PersistenceOptions options = default);
        PersistenceResult Delete(string key);
        PersistenceResult DeleteAll(string prefix);

        void Save();

        // 异步操作
        IConvaiOperation<PersistenceResult>   SyncAsync(CancellationToken ct = default);
        IConvaiOperation<PersistenceResult>   SaveVersionedAsync<T>(VersionedKey key, T value,
                                                  ConflictResolutionStrategy strategy = ConflictResolutionStrategy.LastWriteWins,
                                                  CancellationToken ct = default);
        IConvaiOperation<VersionedValue<T>>   LoadVersionedAsync<T>(string ns, string key, CancellationToken ct = default);
        IConvaiOperation<PersistenceResult>   MigrateAsync(int fromVersion, int toVersion, CancellationToken ct = default);
    }
}
```

`builder.UsePersistence()` 接受 `IPersistenceProvider`. 如果你的实现是 `IKeyValueStore`，请将其包装在适配器中（见 [适配器模式](#adapter-pattern-for-ikeyvaluestore-implementations) 下文）。异步操作（`SyncAsync`, `SaveVersionedAsync`）如果你的后端是同步的，可以返回占位结果。

**应实现哪个接口：**

| 场景                            | 实现                     |
| ----------------------------- | ---------------------- |
| 本地文件、加密 SQLite、内存             | `IKeyValueStore`       |
| 云端保存、服务端存储、多设备同步              | `IPersistenceProvider` |
| 现有 `IKeyValueStore` 并在其上添加云同步 | 两者都要——将同步/版本化操作委托给云客户端 |

### 支持类型

#### PersistenceResult

| 成员                                     | 类型         | 说明                           |
| -------------------------------------- | ---------- | ---------------------------- |
| `Success`                              | `bool`     | 操作是否成功。                      |
| `错误消息`                                 | `string`   | 错误描述，如果 `Success` 为 `false`. |
| `时间戳`                                  | `DateTime` | 操作完成时的时间。                    |
| `版本`                                   | `long`     | 操作后的版本号（适用于版本化操作）。           |
| `PersistenceResult.Succeeded(version)` | static     | 创建成功结果。                      |
| `PersistenceResult.Failed(error)`      | static     | 创建失败结果。                      |

#### ConflictResolutionStrategy

由……使用 `SaveVersionedAsync` 用于在异步/云场景中解决写入冲突。

| 值                    | 行为                 |
| -------------------- | ------------------ |
| `LastWriteWins`      | 基于时间戳，最近写入的值获胜。    |
| `HighestVersionWins` | 版本号更高的值获胜。         |
| `LocalWins`          | 本地数据始终覆盖远程数据。      |
| `RemoteWins`         | 远程数据始终覆盖本地数据。      |
| `Manual`             | 向调用方返回冲突信息，以便显式解决。 |

#### PersistenceOptions

```csharp
var options = new PersistenceOptions(
    conflictPolicy:  ConflictResolutionPolicy.LastWriteWins,
    createIfMissing: true,
    maxRetries:      3
);
```

### 实现示例

#### 内存存储（测试 / CI）

适用于自动化测试和 CI 运行，在这些场景下，运行之间的持久状态会破坏结果。

```csharp
// InMemoryKeyValueStore.cs
using System.Collections.Generic;
using Convai.Domain.Abstractions;

public class InMemoryKeyValueStore : IKeyValueStore
{
    private readonly Dictionary<string, string> _store = new();

    public string GetString(string key, string defaultValue = null)
        => _store.TryGetValue(key, out string v) ? v : defaultValue;

    public void SetString(string key, string value)
        => _store[key] = value;

    public bool HasKey(string key)
        => _store.ContainsKey(key);

    public void DeleteKey(string key)
        => _store.Remove(key);

    public void Save() { /* 内存中无操作 */ }
}
```

#### 加密文件存储

满足禁止明文的合规要求 `PlayerPrefs` 用于会话数据。

```csharp
// EncryptedFileKeyValueStore.cs
using System.Collections.Generic;
using System.IO;
using Convai.Domain.Abstractions;
using UnityEngine;

public class EncryptedFileKeyValueStore : IKeyValueStore
{
    private readonly string _filePath;
    private readonly IEncryptionService _encryption;
    private Dictionary<string, string> _cache = new();

    public EncryptedFileKeyValueStore(string fileName, IEncryptionService encryption)
    {
        _filePath   = Path.Combine(Application.persistentDataPath, fileName);
        _encryption = encryption;
        Load();
    }

    public string GetString(string key, string defaultValue = null)
        => _cache.TryGetValue(key, out string v) ? v : defaultValue;

    public void SetString(string key, string value) => _cache[key] = value;
    public bool HasKey(string key)                  => _cache.ContainsKey(key);
    public void DeleteKey(string key)               => _cache.Remove(key);

    public void Save()
    {
        string json      = JsonUtility.ToJson(new SerializableDictionary(_cache));
        string encrypted = _encryption.Encrypt(json);
        File.WriteAllText(_filePath, encrypted);
    }

    private void Load()
    {
        if (!File.Exists(_filePath)) return;
        string encrypted = File.ReadAllText(_filePath);
        string json      = _encryption.Decrypt(encrypted);
        _cache           = JsonUtility.FromJson<SerializableDictionary>(json)?.ToDictionary()
                           ?? new Dictionary<string, string>();
    }
    // 为简洁起见，SerializableDictionary 辅助代码省略。
}
```

调用 `Save()` 在每次写入后，或定期刷新。写入会缓冲在内存中——自上次写入以来的数据 `Save()` 会在崩溃时丢失。

### IKeyValueStore 实现的适配器模式

`builder.UsePersistence()` 需要 `IPersistenceProvider`. 使用此适配器包装任何 `IKeyValueStore`:

```csharp
// KeyValueStorePersistenceAdapter.cs
using System.Threading;
using Convai.Domain.Abstractions;
using Convai.Runtime.Core.Async;
using Convai.Runtime.Core.Providers;

public class KeyValueStorePersistenceAdapter : IPersistenceProvider
{
    private readonly IKeyValueStore _store;

    public KeyValueStorePersistenceAdapter(IKeyValueStore store) => _store = store;

    public string GetString(string key, string defaultValue = null)
        => _store.GetString(key, defaultValue);

    public int GetInt(string key, int defaultValue = 0)
    {
        string raw = _store.GetString(key);
        return raw != null && int.TryParse(raw, out int v) ? v : defaultValue;
    }

    public float GetFloat(string key, float defaultValue = 0f)
    {
        string raw = _store.GetString(key);
        return raw != null && float.TryParse(raw, out float v) ? v : defaultValue;
    }

    public bool GetBool(string key, bool defaultValue = false)
    {
        string raw = _store.GetString(key);
        return raw != null && bool.TryParse(raw, out bool v) ? v : defaultValue;
    }

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

    public PersistenceResult SetString(string key, string value, PersistenceOptions options = default)
    { _store.SetString(key, value); return PersistenceResult.Succeeded(); }

    public PersistenceResult SetInt(string key, int value, PersistenceOptions options = default)
    { _store.SetString(key, value.ToString()); return PersistenceResult.Succeeded(); }

    public PersistenceResult SetFloat(string key, float value, PersistenceOptions options = default)
    { _store.SetString(key, value.ToString("G")); return PersistenceResult.Succeeded(); }

    public PersistenceResult SetBool(string key, bool value, PersistenceOptions options = default)
    { _store.SetString(key, value.ToString()); return PersistenceResult.Succeeded(); }

    public PersistenceResult Delete(string key)
    { _store.DeleteKey(key); return PersistenceResult.Succeeded(); }

    public PersistenceResult DeleteAll(string prefix)
        => PersistenceResult.Failed("此后端不支持 DeleteAll。");

    public void Save() => _store.Save();

    // 异步存根——同步后端返回即时结果。
    public IConvaiOperation<PersistenceResult> SyncAsync(CancellationToken ct = default)
        => ConvaiOperation.Succeeded(PersistenceResult.Succeeded());

    public IConvaiOperation<PersistenceResult> SaveVersionedAsync<T>(VersionedKey key, T value,
        ConflictResolutionStrategy strategy = ConflictResolutionStrategy.LastWriteWins,
        CancellationToken ct = default)
        => ConvaiOperation.Succeeded(PersistenceResult.Failed("此后端不支持版本化操作。"));

    public IConvaiOperation<VersionedValue<T>> LoadVersionedAsync<T>(string ns, string key, CancellationToken ct = default)
        => ConvaiOperation.Succeeded(VersionedValue<T>.NotFound);

    public IConvaiOperation<PersistenceResult> MigrateAsync(int fromVersion, int toVersion, CancellationToken ct = default)
        => ConvaiOperation.Succeeded(PersistenceResult.Succeeded());
}
```

`DeleteAll(string prefix)` 在此适配器中返回失败结果。SDK 会在 `DeleteAll` 会话重置操作期间调用。若您的部署需要完整的会话重置，请实现 `DeleteAll` 通过遍历存储的键并移除与前缀匹配的项。

### 注册提供器

```csharp
// CustomPersistenceManager.cs
using Convai.Runtime.Components;
using Convai.Runtime.Core;

public class CustomPersistenceManager : ConvaiManager
{
    [SerializeField] private bool _useEncryptedStorage = true;

    protected override ConvaiRuntimeBuilder CreateRuntimeBuilder()
    {
        ConvaiRuntimeBuilder builder = base.CreateRuntimeBuilder();

        如果
        {
            IEncryptionService encryption = new AesEncryptionService();
            var store = new EncryptedFileKeyValueStore("convai_session.dat", encryption);
            builder.UsePersistence(new KeyValueStorePersistenceAdapter(store));
        }

        return builder;
    }
}
```

### 故障排查

| 症状                                                    | 可能原因                                   | 修复                                                 |
| ----------------------------------------------------- | -------------------------------------- | -------------------------------------------------- |
| 重启后会话未恢复                                              | `GetString` 返回 `null` 用于重新加载会话键        | 确保 `Save()` 在应用退出前同步调用。订阅 `Application.quitting` 。 |
| `NullReferenceException` 内部 `IPersistenceProvider` 实现 | 在存储初始化之前调用异步方法                         | 在提供器的构造函数中初始化后端存储，在 `UsePersistence()` 被调用。        |
| 崩溃时的数据丢失                                              | `SetString` 写入会缓冲在内存中，并且 `Save()` 未被调用 | 调用 `Save()` 在每次写入后，或通过定时器刷新。                       |
| 会话重置无法清除所有 SDK 数据                                     | `DeleteAll(prefix)` 在适配器中返回失败结果        | 实现 `DeleteAll` 通过遍历存储的键集合并移除前缀匹配项。                 |

### 下一步

{% content-ref url="/pages/b6e1d5b82a92f7950701bd2576065db6c0349005" %}
[凭据、身份与存储](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/advanced-topics/custom-providers.md)
{% endcontent-ref %}

{% content-ref url="/pages/007b237f8ee6f8a26b307152ee716a33905cd6da" %}
[运行时模块系统](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/advanced-topics/extending-the-sdk.md)
{% endcontent-ref %}

{% content-ref url="/pages/c94f3576730a705ef8d7adec01d712a12612b542" %}
[实现自定义模块](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/advanced-topics/implement-a-custom-module.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:

```
GET https://docs.convai.com/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/advanced-topics/custom-providers/custom-persistence-provider.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.
