> 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/personal-access-token.md).

# 个人访问令牌

从你的后端生成短期令牌，这样真实 API 密钥就不会随 Unity 构建一起发布，从而消除客户端应用中的凭证泄露。

默认情况下，Convai Unity SDK 会从 `ConvaiSettings.asset`读取你的 API 密钥，它会被编译进你的构建中。任何提取该构建的人都可以取回该密钥，并将其用于你的 Convai 账户。个人访问令牌（PAT）可完全消除这种暴露。你的真实 API 密钥保存在你的 **后端** ——一个由你控制的服务端应用（Node.js、Python、.NET 等），你的用户不会直接与之交互。后端会生成一个短期令牌——有效期为一小时——并将其交给 Unity 应用。应用使用该令牌连接 Convai。如果令牌被截获，它会在一小时内过期，且不能用于访问你的账户设置、角色或计费。PAT 需要一个持有真实 API 密钥的服务器，并代表你的用户调用 Convai 的令牌端点。一个轻量级函数（AWS Lambda、Azure Function、Cloudflare Worker 等）就足够了——它每个会话只需发起一次 HTTP 请求。

```
你的后端  ──持有──►  真实 API 密钥
      │
      │  POST /user/connect  （服务端执行，绝不来自客户端）
      ▼
   Convai API  ──返回──►  apiAuthToken  （1 小时）
      │
      │  在运行时传递给 Unity 应用
      ▼
Unity 应用  ──使用──►  apiAuthToken  作为凭据
                      （真实 API 密钥绝不会包含在构建中）
```

***

### 令牌端点

这三个端点都指向 `https://api.convai.com` 并且需要你的真实 API 密钥作为 `CONVAI-API-KEY` 请求头。 **这些调用由你的后端发起，而不是由 Unity 应用发起。**

#### 生成令牌

```http
POST https://api.convai.com/user/connect
```

| 标头               | 值                  |
| ---------------- | ------------------ |
| `Content-Type`   | `application/json` |
| `CONVAI-API-KEY` | 你的 Convai API 密钥   |

**请求体：** `{}`

**响应：**

```json
{
  "apiAuthToken": "eyJhbGciOi...",
  "expirationTime": "2024-01-15T14:30:00Z"
}
```

| 字段               | 说明                        |
| ---------------- | ------------------------- |
| `apiAuthToken`   | 要传递给 Unity 应用的短期令牌。       |
| `expirationTime` | 过期的 UTC 时间戳——大约在生成后 1 小时。 |

你可以在当前令牌仍然有效时生成新的令牌。生成新令牌不会使旧令牌失效。

#### 延长令牌有效期

```http
POST https://api.convai.com/user/extend-token
```

请求头：与“生成令牌”相同。

```json
{
  "apiAuthToken": "eyJhbGciOi..."
}
```

在不使现有令牌失效的情况下重置其过期计时。

#### 撤销令牌

```http
POST https://api.convai.com/user/revoke-token
```

请求头：与“生成令牌”相同。

```json
{
  "apiAuthToken": "eyJhbGciOi..."
}
```

立即使令牌失效。请在登出时或令牌不再需要时调用此操作。

***

### 与 Unity SDK 集成

获取 `apiAuthToken` 从你的后端获取，然后将其传递给 `ConvaiManager.ActiveManager.ConnectWithAuthTokenAsync()` ——SDK 会将其作为 `CONVAI-API-KEY` 该连接尝试的凭据请求头，正是 API 密钥本应占据的位置。此操作要求在 Project Settings 中选择 Auth Token 模式；参见 [配置 Auth Token 模式](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/authentication/configure-auth-token-mode.md) 以及完整的参数参考中的 [使用现有身份验证令牌进行连接](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/authentication/connect-with-auth-token.md).

**不要在 `ConvaiSettings.asset` 生产构建中设置 API 密钥。** 请将该字段留空，并在连接时提供 PAT。

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

public class PatSessionBootstrapper : MonoBehaviour
{
    [SerializeField] private string _tokenEndpoint = "https://your-backend.com/session/convai-token";

    public async Task ConnectAsync(string endUserId, string endUserName, CancellationToken cancellationToken = default)
    {
        string accessToken = await FetchTokenFromBackendAsync();
        if (string.IsNullOrEmpty(accessToken))
        {
            Debug.LogError("[PatSessionBootstrapper] 没有访问令牌——连接已中止。 " +
                           "请确保你的后端令牌端点可访问。");
            return;
        }

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

    private async Task<string> FetchTokenFromBackendAsync()
    {
        using var request = UnityWebRequest.PostWwwForm(_tokenEndpoint, string.Empty);

        // 使用应用的会话凭据向你自己的后端进行身份验证。
        // 后端使用真实的 Convai API 密钥（绝不会在此处发送）调用 /user/connect。
        request.SetRequestHeader("Authorization", $"Bearer {GetSessionBearer()}");

        var operation = request.SendWebRequest();
        while (!operation.isDone) await Task.Yield();

        if (request.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError($"[PatSessionBootstrapper] 令牌获取失败：{request.error}");
            return null;
        }

        var response = JsonUtility.FromJson<TokenResponse>(request.downloadHandler.text);
        return response?.token;
    }

    // 根据你的应用存储自身会话凭据的方式进行替换（例如来自登录）。
    private static string GetSessionBearer()
        => PlayerPrefs.GetString("session_bearer", string.Empty);

    [System.Serializable]
    private class TokenResponse { public string token; }
}
```

没有 `ConvaiManager` 需要子类—— `ConnectWithAuthTokenAsync` 它本身不获取任何内容，因此令牌获取和连接调用都保留在一个普通组件中。

{% hint style="danger" %}
绝不要直接调用 `https://api.convai.com/user/connect` 从 Unity 应用中直接调用。这样做会要求真实 API 密钥包含在构建中——而这正是 PAT 的目的：防止这种情况。务必从你的后端调用它。
{% endhint %}

{% hint style="danger" %}
不要将 `apiAuthToken` 持久化到磁盘（例如， `PlayerPrefs`）。缓存的令牌也是已存储的凭据。每次应用启动时都应从后端获取新的令牌。
{% endhint %}

***

### 令牌过期与会话时长

一旦 Convai 会话开始，在该会话期间就不再检查令牌——会话进行中到期的令牌不会断开用户连接。PAT 仅在连接时使用。

| 场景                                        | 行为                                |
| ----------------------------------------- | --------------------------------- |
| 令牌在之前过期 `ConnectWithAuthTokenAsync()` 被调用 | 连接失败——请从你的后端获取新令牌并重试。             |
| 令牌在活动会话期间过期                               | 会话不受影响——令牌只在连接时检查，不会在整个会话期间保持有效性。 |
| 令牌过期后应用重启                                 | 启动时务必获取新的令牌——不要在多次启动之间缓存令牌。       |

***

### 使用示例

#### 示例 1：具有每会话令牌的 LMS 平台

某企业安全培训平台会在 LMS 登录响应中发放一个 Convai PAT。令牌会在学习者完成身份验证时由服务器生成，并与 LMS 会话数据一并传递。

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

public class LmsSessionBootstrapper : MonoBehaviour
{
    private async void Start()
    {
        // LmsAuthService.LoginAsync() 会调用你的后端。
        // 你的后端生成一个 Convai PAT，并随会话一起返回。
        LmsSession session = await LmsAuthService.LoginAsync();

        try
        {
            // 身份与学习者记录绑定——长期记忆会跟随学习者。
            await ConvaiManager.ActiveManager.ConnectWithAuthTokenAsync(
                session.ConvaiApiAuthToken, session.LearnerId, session.LearnerDisplayName);
        }
        catch (ConvaiOperationException exception)
        {
            Debug.LogError($"[LmsSessionBootstrapper] 连接失败：{exception.Message}");
        }
    }
}
```

#### 示例 2：共享信息亭，按住户轮换令牌

每位住户登录共享培训信息亭后，都会从后端获得一个新的 PAT，进行模拟交互，然后登出。登出时，令牌会被显式撤销，因此无法再次使用。

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

public class KioskSessionManager : MonoBehaviour
{
    private const string RevokeUrl = "https://api.convai.com/user/revoke-token";

    // 返回给已认证住户的新 PAT 的后端端点。
    [SerializeField] private string _backendTokenEndpoint = "https://your-backend.com/kiosk/convai-token";

    private string _currentToken;

    public async void OnResidentLogin(string residentId, string residentDisplayName, string residentSessionBearer)
    {
        _currentToken = await FetchTokenAsync(residentSessionBearer);

        if (string.IsNullOrEmpty(_currentToken))
        {
            Debug.LogError("[KioskSessionManager] 令牌获取失败——无法开始会话。");
            return;
        }

        try
        {
            await ConvaiManager.ActiveManager.ConnectWithAuthTokenAsync(
                _currentToken, residentId, residentDisplayName);
        }
        catch (ConvaiOperationException exception)
        {
            Debug.LogError($"[KioskSessionManager] 连接失败：{exception.Message}");
        }
    }

    public async void OnResidentLogout()
    {
        await ConvaiManager.ActiveManager.DisconnectAsync();

        if (!string.IsNullOrEmpty(_currentToken))
        {
            await RevokeTokenAsync(_currentToken);
            _currentToken = null;
        }
    }

    private async Task<string> FetchTokenAsync(string residentBearer)
    {
        using var request = UnityWebRequest.PostWwwForm(_backendTokenEndpoint, string.Empty);
        request.SetRequestHeader("Authorization", $"Bearer {residentBearer}");

        var operation = request.SendWebRequest();
        while (!operation.isDone) await Task.Yield();

        if (request.result != UnityWebRequest.Result.Success) return null;

        var response = JsonUtility.FromJson<TokenResponse>(request.downloadHandler.text);
        return response?.token;
    }

    private static async Task RevokeTokenAsync(string token)
    {
        // 注意：撤销需要真实 API 密钥——此调用也应当
        // 通过你的后端代理以获得最高安全性。
        // 这里为清晰起见直接调用；生产环境中请移至后端。
        string body = JsonUtility.ToJson(new RevokeBody { apiAuthToken = token });

        using var request = new UnityWebRequest(RevokeUrl, "POST");
        request.uploadHandler   = new UploadHandlerRaw(Encoding.UTF8.GetBytes(body));
        request.downloadHandler = new DownloadHandlerBuffer();
        request.SetRequestHeader("Content-Type",   "application/json");
        request.SetRequestHeader("CONVAI-API-KEY", ""); // 生产环境请通过后端代理提供。

        var operation = request.SendWebRequest();
        while (!operation.isDone) await Task.Yield();
    }

    [System.Serializable] private class TokenResponse { public string token; }
    [System.Serializable] private class RevokeBody    { public string apiAuthToken; }
}
```

#### 示例 3：用于长时间运行应用的按需令牌刷新

工业培训仿真可以运行数小时。虽然活动会话不受令牌过期影响，但在过期后开始的新会话需要新的令牌。在重新连接之前，请使用你后端的延长或重新生成端点。

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

public class LongRunningSessionManager : MonoBehaviour
{
    [SerializeField] private PatSessionBootstrapper _patBootstrapper;

    // 在开始新会话之前调用，例如在场景重新加载或角色切换之后。
    public async void StartNewSession(string endUserId, string endUserName)
    {
        // 断开任何现有会话。
        await ConvaiManager.ActiveManager.DisconnectAsync();

        // PatSessionBootstrapper.FetchTokenFromBackendAsync() 每次调用都会获取新的令牌。
        // 你的后端可以调用 /user/extend-token 或生成新的令牌——两者都可以。
        await _patBootstrapper.ConnectAsync(endUserId, endUserName);
    }
}
```

***

### 故障排除

| 症状                                    | 可能原因                                                | 修复方法                                                                          |
| ------------------------------------- | --------------------------------------------------- | ----------------------------------------------------------------------------- |
| 连接立即失败                                | `apiAuthToken` 为空——后端获取失败                           | 请在控制台中查看 `令牌获取失败` 日志；请验证你的后端端点 URL 和认证请求头。                                    |
| `401` 连接时来自 Convai 的认证错误              | 令牌在之前已经过期或被撤销 `ConnectWithAuthTokenAsync()`         | 务必在连接前立即获取新的令牌——绝不要在不同会话之间复用缓存的令牌。                                            |
| 连接失败并出现 `ConfigAuthTokenModeRequired` | 项目的 `ConvaiSettings` 资源仍然保留 `AuthMode` 设置为 `ApiKey` | 设置 `AuthMode` 为 `AuthToken` 在 **编辑 > 项目设置 > Convai SDK > 凭据**，即使那里没有配置端点 URL。 |
| `apiAuthToken` 在后端响应中为空               | 请求正文格式错误或缺少 `CONVAI-API-KEY` 后端调用中的请求头              | 确保正文为 `{}` 并且该请求头已存在。请在后端记录原始响应以检查错误。                                         |
| 令牌在开发环境可用，但在生产构建中失败                   | `ConvaiSettings.asset` API 密钥字段为空，且未获取 PAT          | 请确认令牌获取调用已完成，并且其结果已传递给 `ConnectWithAuthTokenAsync()` ，然后应用再尝试连接。              |

***

### 下一步

{% 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/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/personal-access-token.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.
