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

# 自定义凭证提供器

覆盖 SDK 的默认凭证来源，以便从环境变量、密钥库或任何运行时解析的凭证存储中提供 API 密钥。

默认情况下，Convai Unity SDK 会从 `ConvaiSettings.asset`，存储在 `Assets/Resources/`。某些部署场景要求凭据来自其他地方——CI 环境变量、密钥管理器或按租户配置的配置服务。

{% hint style="info" %}
如果你需要的是在每次连接前从你自己的服务器解析出的短期凭据，SDK 已内置支持这一流程，你不需要自定义凭据提供程序。请参见 [身份验证](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/authentication.md)，以及 [编写自定义令牌提供程序](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/authentication/custom-token-provider.md) 以了解它提供的扩展点。对于该路径未覆盖的情况，请使用本页上的接口——例如从设置资源以外的地方提供项目 API 密钥。
{% endhint %}

### 前置条件

* 一个可正常运行的 Convai 场景，带有一个 `ConvaiManager` GameObject 上的组件
* 下面的同步示例会创建一个……的子类 `ConvaiManager` ——将新组件添加到同一个 GameObject，并移除原始的 `ConvaiManager` 组件。异步示例会调用 `ConvaiManager.ActiveManager` ，而是从普通组件调用，因此不需要子类。

如果你还没有设置场景，请先查看 [入门指南](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/getting-started.md) 。

### 凭据如何在 SDK 中传递

当运行时构建时， `ConvaiBootstrapConfigSnapshot` 会将 API 密钥和服务器 URL 作为一个不可变的配对进行捕获。构建完成后，这些值会通过以下方式暴露给模块和内部服务： `ICredentialProvider`:

```csharp
public interface ICredentialProvider
{
    bool HasValidCredentials { get; }
    string GetApiKey();
    string GetServerUrl();
    void Refresh();
}
```

`GetApiKey()` 和 `GetServerUrl()` 会在连接时调用——不是每一帧都调用。 `HasValidCredentials` 会决定是否允许连接尝试：如果它返回 `false`，SDK 将不会尝试连接。 `Refresh()` 当 SDK 检测到与凭据相关的错误，并希望提供程序从其来源重新加载时，会调用它。

你不需要直接实现 `ICredentialProvider` 。你在构建运行时时提供凭据值，SDK 会基于这些值在内部创建提供程序。

当凭据必须异步解析时——例如在连接尝试前刚从后端获取的令牌——SDK 会通过第二个附加接口来解析它， `IAsyncCredentialProvider`。它的 `EnsureCredentialsAsync(CancellationToken)` 方法会在连接流程中被等待，先于 `GetApiKey()` 被读取，因此 SDK 不需要针对这种情况的异步 `ConvaiManager` 生命周期钩子。你同样不需要直接实现这个接口——请查看下面的异步示例以及 [编写自定义令牌提供程序](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/authentication/custom-token-provider.md) ，了解在连接时必须获取的凭据的支持提供方式。

### 提供自定义凭据

重写 `CreateRuntimeBuilder()` 在一个 `ConvaiManager` 子类，并调用 `builder.UseConfig()` 并传入一个 `ConvaiBootstrapConfigSnapshot` ，该对象由你的凭据来源构建。务必先调用 `base.CreateRuntimeBuilder()` ——它会处理特定平台的传输选择、事件系统设置以及其他你无需复制的连接工作。调用 `UseConfig()` 之后只会覆盖凭据快照。

```csharp
// EnvironmentCredentialManager.cs
using Convai.Runtime.Components;
using Convai.Runtime.Core;
using Convai.Runtime.Core.Configuration;
using UnityEngine;

public class EnvironmentCredentialManager : ConvaiManager
{
    protected override ConvaiRuntimeBuilder CreateRuntimeBuilder()
    {
        ConvaiRuntimeBuilder builder = base.CreateRuntimeBuilder();

        string apiKey    = ResolveApiKey();
        string serverUrl = ResolveServerUrl();

        if (string.IsNullOrEmpty(apiKey))
        {
            Debug.LogError("[EnvironmentCredentialManager] 未找到 API 密钥。 " +
                           "请检查你的环境或密钥配置。");
        }

        builder.UseConfig(new ConvaiBootstrapConfigSnapshot(
            apiKey:    apiKey,
            serverUrl: serverUrl
        ));

        return builder;
    }

    private static string ResolveApiKey()
    {
        // 从环境变量读取（CI、Docker、云运行）。
        string key = System.Environment.GetEnvironmentVariable("CONVAI_API_KEY");
        if (!string.IsNullOrEmpty(key)) return key;

        // 回退到 ConvaiSettings（编辑器 / 本地开发）。
        return ConvaiSettings.Instance?.ApiKey ?? string.Empty;
    }

    private static string ResolveServerUrl()
    {
        return System.Environment.GetEnvironmentVariable("CONVAI_SERVER_URL")
               ?? ConvaiSettings.Instance?.ServerUrl
               ?? "https://live.convai.com";
    }
}
```

在层级视图中，找到包含以下组件的 GameObject： `ConvaiManager` 。添加 `EnvironmentCredentialManager` 作为新组件，然后移除原始的 `ConvaiManager` 组件。该子类继承了所有 `ConvaiManager` 功能——场景中其他任何内容都无需更改。

### ConvaiBootstrapConfigSnapshot 参数

`ConvaiBootstrapConfigSnapshot` 是不可变的——所有值都在构造时设置，并且在运行时启动后无法更改。

| 参数                         | 类型                     | 默认     | 说明                                          |
| -------------------------- | ---------------------- | ------ | ------------------------------------------- |
| `apiKey`                   | `string`               | —      | **必填。** 你的 Convai API 密钥。                   |
| `serverUrl`                | `string`               | —      | **必填。** Convai 实时服务器 URL。                   |
| `connectionType`           | `ConvaiConnectionType` | `音频`   | 是仅通过音频连接，还是通过音频 + 视频连接。                     |
| `serverEndpoint`           | `ConvaiServerEndpoint` | `连接`   | 服务器端点变体。除非另有指示，否则保持默认。                      |
| `connectionTimeoutSeconds` | `float`                | `30f`  | 在连接尝试被视为失败之前的超时时间。                          |
| `globalLogLevel`           | `LogLevel`             | `信息`   | SDK 初始日志级别。可在运行时通过以下方式更改： `ConvaiSettings`. |
| `enableSessionResume`      | `bool`                 | `true` | SDK 是否应尝试恢复之前的会话。                           |
| `maxRetryAttempts`         | `int`                  | `3`    | 放弃前的最大重连尝试次数。                               |

`ConvaiBootstrapConfigSnapshot` 会在启动时捕获。如果你的凭据来源发放短期令牌，SDK 无法在会话中自动轮换它们。请将令牌有效期设计得长于预期最长会话，或者断开并重新连接以应用刷新后的令牌。

{% hint style="danger" %}
切勿将你的 API 密钥记录到 Unity Console 或日志文件中，也不要将其序列化。 `ConvaiBootstrapConfigSnapshot` 会刻意从其 `ToString()` 输出中省略 API 密钥。
{% endhint %}

### 使用示例

#### 示例 1：带本地回退的环境变量

如上所示于 [提供自定义凭据](#provide-custom-credentials)。最适合在以环境变量注入密钥的 CI/CD 流水线和基于 Docker 的部署中使用。

#### 示例 2：启动前从密钥保险库获取

某些部署会在启动时从密钥服务拉取凭据。 `CreateRuntimeBuilder()` 会在以下内部同步运行： `ConvaiManager.Awake()`，在任何 `async` 代码完成之前，因此 `ConvaiManager` 子类不能在这里等待保险库调用。请改为在普通组件中异步解析凭据，然后将解析出的值直接传给 `ConvaiManager.ActiveManager.ConnectWithAuthTokenAsync()` ——这与文档中介绍的显式凭据连接路径相同，见 [使用现有身份验证令牌进行连接](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/authentication/connect-with-auth-token.md)。这要求在 Project Settings 中选择 Auth Token 模式；请参见 [配置 Auth Token 模式](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/authentication/configure-auth-token-mode.md).

```csharp
// VaultSessionBootstrapper.cs
using System.Threading;
using System.Threading.Tasks;
using Convai.Runtime.Components;
using Convai.Runtime.Core.Async;
using UnityEngine;

public class VaultSessionBootstrapper : MonoBehaviour
{
    [SerializeField] private string _vaultEndpoint = "https://vault.internal/v1/convai";

    public async Task ConnectAsync(string endUserId, string endUserName, CancellationToken cancellationToken = default)
    {
        string credential = await FetchCredentialFromVaultAsync();
        if (string.IsNullOrEmpty(credential))
        {
            Debug.LogError("[VaultSessionBootstrapper] 保险库未返回凭据——连接已中止。");
            return;
        }

        try
        {
            await ConvaiManager.ActiveManager.ConnectWithAuthTokenAsync(
                credential, endUserId, endUserName, cancellationToken);
        }
        catch (ConvaiOperationException exception)
        {
            Debug.LogError($"[VaultSessionBootstrapper] 连接失败：{exception.Message}");
        }
    }

    private async Task<string> FetchCredentialFromVaultAsync()
    {
        using var client = new System.Net.Http.HttpClient();
        try
        {
            string json = await client.GetStringAsync(_vaultEndpoint);
            var response = JsonUtility.FromJson<VaultResponse>(json);
            return response.ApiKey;
        }
        catch (System.Exception ex)
        {
            Debug.LogError($"[VaultSessionBootstrapper] 获取凭据失败：{ex.Message}");
            return null;
        }
    }

    [System.Serializable]
    private class VaultResponse { public string ApiKey; }
}
```

没有 `ConvaiManager` 需要子类—— `ConnectWithAuthTokenAsync` 会直接接受解析出的凭据，用于那一次连接尝试。

#### 示例 3：来自配置服务的按租户凭据

在多租户部署中，每个客户都有不同凭据时，可以在连接前从租户配置端点解析它，然后将解析出的值直接传给 `ConnectWithAuthTokenAsync()`.

```csharp
// TenantSessionBootstrapper.cs
using System.Threading;
using System.Threading.Tasks;
using Convai.Runtime.Components;
using Convai.Runtime.Core.Async;
using UnityEngine;

public class TenantSessionBootstrapper : MonoBehaviour
{
    [SerializeField] private TenantConfigService _configService;

    public async Task ConnectAsync(string endUserId, string endUserName, CancellationToken cancellationToken = default)
    {
        TenantConfig config = await _configService.LoadAsync();

        try
        {
            await ConvaiManager.ActiveManager.ConnectWithAuthTokenAsync(
                config.ConvaiCredential, endUserId, endUserName, cancellationToken);
        }
        catch (ConvaiOperationException exception)
        {
            Debug.LogError($"[TenantSessionBootstrapper] 连接失败：{exception.Message}");
        }
    }
}
```

与保险库示例一样，这要求在 Project Settings 中选择 Auth Token 模式。如果场景中的每次连接都应自动解析租户凭据，而无需修改调用点，则请注册一个 `IConvaiAuthTokenProvider` 来替代——请参见 [编写自定义令牌提供程序](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/authentication/custom-token-provider.md).

### 故障排除

| 症状                                     | 可能原因                                                                                  | 修复方法                                                                      |
| -------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `无法启动：适配器未初始化。` 在控制台中                  | `StartRuntimeAsync()` 在以下之前被调用： `ConvaiManager.Awake()` 运行时和适配器完成构建——例如，某个自定义脚本直接调用了它 | 让 `ConvaiManager`的内置生命周期调用 `StartRuntimeAsync()`。不要在以下之前手动调用它： `Start()`. |
| 会话已连接，但 Convai 立即返回身份验证错误              | 传递给快照的 API 密钥为空或不正确                                                                   | 记录解析后的密钥 **长度** （不是值本身），以确认在构建前已成功填充。                                     |
| `IsValid` 返回 `false` 在配置快照上            | `apiKey` 或 `serverUrl` 为 null 或为空                                                     | 在你的解析方法中添加 null 检查和回退。                                                    |
| `ConvaiSettings.Instance` 在构建版本中为 null | `ConvaiSettings.asset` 未出现在 `Assets/Resources/`                                       | 仅在以下情况下使用 `ConvaiSettings.Instance` 作为编辑器/开发环境中的回退；切勿在生产构建中将其作为唯一来源。      |

### 下一步

{% content-ref url="/pages/4f9746367744635226a122f62cd5fa9abd892214" %}
[自定义身份提供器](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/advanced-topics/custom-providers/custom-identity-provider.md)
{% endcontent-ref %}

{% content-ref url="/pages/2e48fc65c96962e4ad153649c1ead0353c2ca940" %}
[自定义持久化提供器](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/advanced-topics/custom-providers/custom-persistence-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-credential-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.
