> 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 跟踪与你自己的认证系统、学习者记录或自助终端登录流程绑定。

Convai 的长期记忆、MAU（每月活跃用户）跟踪以及最终用户管理功能，都依赖于每个用户拥有一个稳定、一致的标识符。对于大多数训练模拟和交互体验，你会希望将默认的基于设备的 ID 替换为对你的应用有意义的 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` 中的 GUID `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()` 在 `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();

    // 在每次连接之前由自助终端登录界面更新。
    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 声明）。
    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` 为 null —— manager `Awake` 尚未运行 | 在 `Start()` 或之后调用设置器，绝不要在 `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 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/advanced-topics/custom-providers/custom-identity-provider.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.
