> 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 会从以下位置读取你的 API 密钥： `ConvaiSettings.asset`，该密钥被编译进你的构建中。任何提取构建的人都可以取出该密钥并用它访问你的 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` ，并且需要在 `CONVAI-API-KEY` 请求头中提供你的真实 API 密钥。 **这些调用由你的后端发起，而不是由 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` 用于生产构建。** 将该字段留空，并在连接时改为提供 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" %}
绝不要直接从 Unity 应用调用 `https://api.convai.com/user/connect` 。这样做需要将真实 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` 为 null —— 后端获取失败                         | 请在控制台查看 `令牌获取失败` 日志；验证你的后端端点 URL 和身份验证请求头。                                                                |
| `401` 连接时来自 Convai 的身份验证错误            | 令牌在之前已经过期或被撤销 `ConnectWithAuthTokenAsync()`             | 务必在连接前立即获取新令牌——切勿在多个会话之间重复使用缓存的令牌。                                                                        |
| 连接失败并出现 `ConfigAuthTokenModeRequired` | 项目的 `ConvaiSettings` asset 仍然设置为 `AuthMode` 设为 `ApiKey` | 设置为 `AuthMode` 到 `AuthToken` 在 **Edit > Project Settings > Convai SDK > Credentials**，即使那里未配置端点 URL 也是如此。 |
| `apiAuthToken` 在后端响应中为 null           | 请求体格式错误或后端调用中缺少 `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 dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
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=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
