> 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).

# 自定义凭据提供程序

默认情况下，Convai Unity SDK 会从以下位置读取 API 密钥和服务器 URL： `ConvaiSettings.asset`，存储于 `Assets/Resources/`。某些部署场景要求凭据来自其他位置——例如 CI 环境变量、secrets 管理器、按租户配置的服务，或提供短期令牌的后端。

### 先决条件

* 一个可工作的 Convai 场景，且其中有一个 `ConvaiManager` 组件位于某个 GameObject 上
* 所有示例都会创建以下类的子类： `ConvaiManager` ——将这个新组件添加到同一个 GameObject 上，并移除原始的 `ConvaiManager` 组件

如果你还没有搭建场景，请先查看 [入门指南](/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 会根据这些值在内部创建提供者。

### 提供自定义凭据

重写 `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 密钥。" +
                           "请检查你的环境或 secrets 配置。");
        }

        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";
    }
}
```

在你的 Hierarchy 中，找到带有 `ConvaiManager` 的 GameObject。添加 `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`    | 放弃之前的最大重连尝试次数。                               |

{% hint style="danger" %}
`ConvaiBootstrapConfigSnapshot` 会在启动时被捕获。如果你的凭据来源发放的是短期令牌，SDK 无法在会话进行中自动轮换它们。请设计你的令牌生命周期长于最长预期会话，或者断开并重新连接以应用刷新后的令牌。
{% endhint %}

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

### 使用示例

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

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

#### 示例 2：启动前从 secrets vault 获取

某些部署会在启动时从 secrets 服务拉取凭据。因为 `ConvaiBootstrapConfigSnapshot` 必须在以下调用之前就绪： `ConvaiManager.Awake()` 调用 `BuildRuntime()`，所以必须在 `base.Awake()` 运行之前异步获取凭据。

```csharp
// VaultCredentialManager.cs
using System.Threading.Tasks;
using Convai.Runtime.Components;
using Convai.Runtime.Core;
using Convai.Runtime.Core.Configuration;
using UnityEngine;

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

    private string _resolvedApiKey;
    private string _resolvedServerUrl = "https://live.convai.com";

    protected override async void Awake()
    {
        await FetchCredentialsAsync();
        base.Awake(); // 使用已解析的凭据触发 BuildRuntime() → CreateRuntimeBuilder()。
    }

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

    protected override ConvaiRuntimeBuilder CreateRuntimeBuilder()
    {
        ConvaiRuntimeBuilder builder = base.CreateRuntimeBuilder();
        builder.UseConfig(new ConvaiBootstrapConfigSnapshot(_resolvedApiKey, _resolvedServerUrl));
        return builder;
    }

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

`base.Awake()` 会在凭据获取完成后显式调用。其他 `Awake()` 方法中任何依赖于 `ConvaiManager.ActiveManager` 已就绪的代码都必须改用 `Start()` 或更后面的阶段。

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

对于多租户部署，每个客户都有不同 API 密钥，可以在场景开始时从租户配置端点解析凭据。

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

public class TenantCredentialManager : ConvaiManager
{
    [SerializeField] private TenantConfigService _configService;

    private string _apiKey;
    private string _serverUrl = "https://live.convai.com";

    protected override async void Awake()
    {
        TenantConfig config = await _configService.LoadAsync();
        _apiKey    = config.ConvaiApiKey;
        _serverUrl = config.ConvaiServerUrl ?? _serverUrl;
        base.Awake();
    }

    protected override ConvaiRuntimeBuilder CreateRuntimeBuilder()
    {
        ConvaiRuntimeBuilder builder = base.CreateRuntimeBuilder();
        builder.UseConfig(new ConvaiBootstrapConfigSnapshot(_apiKey, _serverUrl));
        return builder;
    }
}
```

### 故障排除

| 症状                                          | 可能原因                                            | 修复                                                            |
| ------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------- |
| `[ConvaiManager] 无法启动：适配器未初始化。` 在 Console 中 | `BuildRuntime()` 在凭据解析之前运行                      | 请确保在以下调用之前完成凭据获取： `base.Awake()` 被调用。                         |
| 会话已连接，但 Convai 立即返回身份验证错误                   | 传入快照的 API 密钥为空或不正确                              | 记录已解析的密钥 **长度** （不是值本身），以确认其在构建前已填充。                          |
| `IsValid` 返回 `false` 在配置快照上                 | `apiKey` 或 `serverUrl` 为 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 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-credential-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.
