> 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-identity-provider.md).

# 自定义身份提供程序

Convai 的长期记忆、MAU（每月活跃用户）跟踪以及最终用户管理功能依赖于每个用户一个稳定、一致的标识符。对于大多数训练模拟和交互式体验，你会希望用一个对你的应用有意义的标识替换默认的基于设备的 ID——例如来自你的认证系统的用户 ID、学习者记录编号，或任何能在会话和设备之间唯一标识某个人的稳定字符串。

### 先决条件

* 一个可工作的 Convai 场景，且其中有一个 `ConvaiManager` 组件
* 该 [长期记忆](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/long-term-memory.md) 已在你的角色上启用该功能，以便按用户显示记忆
* 你自己的用户登录或身份系统，用于提供稳定的用户 ID

如果你还没有用户登录系统，默认的基于设备的 ID 就足够了——等认证就绪后再回到这里。

### 身份提供器接口

两个接口控制 SDK 如何识别当前用户。

#### IEndUserIdentityProvider

主要接口。SDK 会调用 `GetEndUserId()` 每次 `ConnectAsync()` 并将结果作为最终用户标识符发送给 Convai。

```csharp
public interface IEndUserIdentityProvider
{
    string GetEndUserId();
}
```

返回字符串的要求：

* 必须非 null 且非空。空字符串会导致连接失败。
* 必须稳定：同一设备上的同一用户（或跨设备）在不同会话中必须返回相同的 ID。
* 必须对每个用户唯一。共享 ID 会破坏跨用户的长期记忆。

{% hint style="warning" %}
如果两个不同用户解析到同一个 ID，他们的长期记忆条目会静默合并。不会报错——只是数据会错。请确保你的 ID 来源在整个用户群中全局唯一。
{% endhint %}

#### IEndUserMetadataProvider

可选。随连接请求向 Convai 提供额外的键值元数据。可用于传递显示名称、角色代码、部门 ID，或任何应随用户记录一起发送的上下文。

```csharp
public interface IEndUserMetadataProvider
{
    IReadOnlyDictionary<string, object> GetEndUserMetadata();
}
```

该字典可以为空，但不能为 `null`。值必须是可 JSON 序列化的原始类型（`string`, `int`, `float`, `bool`）。不可序列化的值会被静默丢弃。

### 默认行为

`DeviceEndUserIdProvider` 是 SDK 的默认值。其行为因上下文而异：

| 上下文                      | 来源                                               | 稳定性                                   |
| ------------------------ | ------------------------------------------------ | ------------------------------------- |
| 玩家构建版本（Android、iOS、PC 等） | `SystemInfo.deviceUniqueIdentifier` 带持久化 GUID 回退 | 按设备保持稳定；在重装操作系统或清除设备后重置               |
| Unity 编辑器                | 存储在 `PlayerPrefs` 中，键为 `convai.end_user_id`      | 在编辑器安装期间保持稳定；如果 `PlayerPrefs` 被清除则会重置 |

在以下情况下替换默认值：

* 你的应用有自己的登录系统，并且会话必须跟随用户而不是设备。
* 多个学习者共享同一台设备（自助终端模式、共享实验室机器）。
* 你需要跨设备连续性（移动端 + 桌面端）。
* 合规要求用户 ID 必须与你自己的主记录系统一致。

### 实现身份提供器

```csharp
// AuthIdentityProvider.cs
using System.Collections.Generic;
using Convai.Domain.Identity;

public class AuthIdentityProvider : IEndUserIdentityProvider, IEndUserMetadataProvider
{
    private readonly IAuthService _authService;

    public AuthIdentityProvider(IAuthService authService)
    {
        _authService = authService;
    }

    // 每次 ConnectAsync() 只调用一次——必须返回一个稳定的非空字符串。
    public string GetEndUserId()
    {
        string userId = _authService.CurrentUserId;

        if (string.IsNullOrEmpty(userId))
            throw new System.InvalidOperationException(
                "用户未认证。请确保登录在连接到 Convai 之前完成。");

        return userId;
    }

    // 每次 ConnectAsync() 只调用一次——如果不需要元数据，则返回空字典。
    public IReadOnlyDictionary<string, object> GetEndUserMetadata()
    {
        return new Dictionary<string, object>
        {
            ["displayName"] = _authService.CurrentUserDisplayName ?? "Unknown",
            ["role"]        = _authService.CurrentUserRole ?? "learner",
            ["department"]  = _authService.CurrentUserDepartment ?? string.Empty
        };
    }
}
```

### 注册提供器

身份提供器可以通过两种方式注册，取决于你是否还需要覆盖其他构建器设置。

#### 直接设置器（更简单）

调用 `SetEndUserIdentityProvider()` 是位于 `SetEndUserMetadataProvider()` on `ConvaiManager.ActiveManager` 在第一次 `ConnectAsync()` 调用之前。这些设置器只要连接尚未建立，就可以在场景生命周期中的任何时刻使用。

```csharp
// AuthSceneInitializer.cs
using Convai.Runtime.Components;
using UnityEngine;

public class AuthSceneInitializer : MonoBehaviour
{
    [SerializeField] private AuthService _authService;

    private async void Start()
    {
        // 确保用户在 Convai 连接之前已登录。
        await _authService.EnsureLoggedInAsync();

        ConvaiManager manager = ConvaiManager.ActiveManager;
        if (manager == null)
        {
            Debug.LogError("[AuthSceneInitializer] 场景中未找到 ConvaiManager。");
            return;
        }

        var provider = new AuthIdentityProvider(_authService);
        manager.SetEndUserIdentityProvider(provider);
        manager.SetEndUserMetadataProvider(provider);

        // 现在可以安全连接——身份已解析。
        await manager.ConnectAsync();
    }
}
```

{% hint style="danger" %}
不要在 `ConnectAsync()` 之前调用。若 `ConvaiManager` 已配置为在启动时自动连接（`ConnectOnStart = true`），请禁用该选项，并在登录完成后手动触发连接。在提供器设置之前连接会导致使用默认的基于设备的 ID，从而静默地将会话关联到错误的身份。
{% endhint %}

#### CreateRuntimeBuilder 覆盖

当你还要自定义其他构建器设置（凭据、持久化、模块）并希望把所有自定义放在一处时，使用这种方法。

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

public class AuthConvaiManager : ConvaiManager
{
    private AuthService _authService;

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

        // _authService 必须在 Awake() 之前解析——在这里注入或查找它。
        _authService = FindObjectOfType<AuthService>();

        if (_authService != null)
        {
            var provider = new AuthIdentityProvider(_authService);
            builder.WithEndUserIdentityProvider(provider);
            builder.WithEndUserMetadataProvider(provider);
        }

        return builder;
    }
}
```

### 使用示例

#### 示例 1：带学习者记录的培训平台

一家企业安全培训平台通过其 LMS 学习者 ID 来识别每位员工。过去会话的记忆（涵盖的主题、犯过的错误）会在多次模拟运行之间持续保留。

```csharp
public class LmsIdentityProvider : IEndUserIdentityProvider, IEndUserMetadataProvider
{
    private readonly LmsSession _session;

    public LmsIdentityProvider(LmsSession session) => _session = session;

    public string GetEndUserId() => _session.LearnerId; // 例如，"emp-12345"

    public IReadOnlyDictionary<string, object> GetEndUserMetadata()
    {
        return new Dictionary<string, object>
        {
            ["name"]     = _session.LearnerName,
            ["courseId"] = _session.CourseId,
            ["cohort"]   = _session.CohortCode
        };
    }
}
```

通过以下方式注册 `manager.SetEndUserIdentityProvider(new LmsIdentityProvider(lmsSession))` 后再连接。

#### 示例 2：带 PIN 登录的共享自助终端

一家医院培训自助终端供多名住院医师共享。每位住院医师用 PIN 登录，与模拟患者互动，然后注销。下一位住院医师会获得一个与其自身身份绑定的全新会话。

```csharp
public class KioskIdentityProvider : IEndUserIdentityProvider
{
    public static KioskIdentityProvider Instance { get; } = new();

    // 每次连接前由自助终端登录 UI 更新。
    public string ActiveResidentId { get; set; }

    public string GetEndUserId()
    {
        if (string.IsNullOrEmpty(ActiveResidentId))
            throw new System.InvalidOperationException("没有住院医师登录。");

        return $"resident-{ActiveResidentId}";
    }
}
```

注销时：断开 Convai 连接，更新 `ActiveResidentId`，然后为下一位住院医师重新连接。

#### 示例 3：移动端 + 桌面端的跨设备连续性

一名学习者在手机上开始合规培训会话，并在桌面工作站上继续。两个设备都从你的后端认证系统解析到同一个稳定用户 ID，因此 Convai 的记忆会跟随用户，无论他们从哪个设备连接。

```csharp
public class BackendAuthIdentityProvider : IEndUserIdentityProvider
{
    private readonly string _stableUserId;

    // stableUserId 从你的后端认证令牌中获取（例如 JWT 的 sub claim）。
    public BackendAuthIdentityProvider(string stableUserId)
    {
        if (string.IsNullOrEmpty(stableUserId))
            throw new System.ArgumentException("用户 ID 不能为空或空字符串。", nameof(stableUserId));

        _stableUserId = stableUserId;
    }

    public string GetEndUserId() => _stableUserId;
}
```

在令牌验证后注册： `manager.SetEndUserIdentityProvider(new BackendAuthIdentityProvider(jwtSubClaim));`

### 故障排查

| 症状                                                      | 可能原因                                                   | 修复                                              |
| ------------------------------------------------------- | ------------------------------------------------------ | ----------------------------------------------- |
| 长期记忆不会在会话之间保留                                           | 身份在会话之间发生变化                                            | 在连接前记录解析出的 ID，以确认它在不同运行之间保持稳定。                  |
| 两个用户共享同一段记忆                                             | 两个不同用户解析到同一个 ID                                        | 确保你的 ID 来源在整个用户群中全局唯一。                          |
| 在设置身份提供器后立即连接失败                                         | `GetEndUserId()` 抛出了异常或返回了空字符串                         | 在 `GetEndUserId()` 中进行 try-catch 包装；记录异常消息。     |
| `NullReferenceException` 在 `SetEndUserIdentityProvider` | `ConvaiManager.ActiveManager` 为空——manager `Awake` 尚未运行 | 在 `Start()` 或之后调用该 setter，切勿在 `Awake()`.        |
| 记忆会从之前的测试人员/设备累积过来                                      | `DeviceEndUserIdProvider` 在注册自定义提供器之前处于活动状态            | 清除 `convai.end_user_id` `PlayerPrefs` 键，然后重新连接。 |

### 下一步

{% content-ref url="/pages/619185ebc41c18d7fc15c71b6aaa2a22367ff0a6" %}
[长期记忆](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/long-term-memory.md)
{% endcontent-ref %}

{% content-ref url="/pages/fdc49f930fecd7fb00d337f46a900594f8f26fb5" %}
[自定义凭据提供程序](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/advanced-topics/custom-providers/custom-credential-provider.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-identity-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.
