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

# 个人访问令牌

默认情况下，Convai Unity SDK 会从 `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 应用发起。**

#### 生成令牌

```
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 小时。 |

当当前令牌仍然有效时，你可以生成一个新令牌。生成新令牌不会使之前的令牌失效。

#### 延长令牌

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

头部：与“生成”相同。

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

重置现有令牌的过期计时，但不会使其失效。

#### 撤销令牌

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

头部：与“生成”相同。

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

会立即使令牌失效。请在注销时，或在不再需要令牌时调用此接口。

***

### 与 Unity SDK 集成

将 `apiAuthToken` 值作为 `apiKey` 参数中 `ConvaiBootstrapConfigSnapshot`传入 `CONVAI-API-KEY` 。SDK 会将其作为

**不要在 `ConvaiSettings.asset` 中为生产构建设置 API 密钥。** 将该字段留空，并在运行时通过以下方式提供 PAT： `CreateRuntimeBuilder()`.

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

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

    private string _accessToken;

    protected override async void Awake()
    {
        _accessToken = await FetchTokenFromBackendAsync();
        base.Awake();
    }

    protected override ConvaiRuntimeBuilder CreateRuntimeBuilder()
    {
        ConvaiRuntimeBuilder builder = base.CreateRuntimeBuilder();

        if (!string.IsNullOrEmpty(_accessToken))
        {
            builder.UseConfig(new ConvaiBootstrapConfigSnapshot(
                apiKey:    _accessToken,
                serverUrl: "https://live.convai.com"
            ));
        }
        else
        {
            Debug.LogError("[PatConvaiManager] 没有访问令牌——连接将失败。 " +
                           "确保你的后端令牌端点可访问。");
        }

        return builder;
    }

    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($"[PatConvaiManager] 令牌获取失败：{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; }
}
```

{% hint style="danger" %}
切勿直接从 Unity 应用调用 `https://api.convai.com/user/connect` 。这样做就需要将真实 API 密钥放入构建中——而 PAT 的存在正是为了避免这一点。务必从你的后端调用它。
{% endhint %}

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

***

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

一旦 Convai 会话开始，在该会话持续期间就不再检查令牌——即使令牌在会话中途过期，也不会使用户断开连接。PAT 只在连接时被使用一次。

| 场景                               | 行为                              |
| -------------------------------- | ------------------------------- |
| 令牌在以下操作之前过期 `ConnectAsync()` 被调用 | 连接失败——从你的后端获取一个新令牌并重试。          |
| 令牌在活动会话期间过期                      | 会话不受影响——令牌只在连接时检查，不会在会话持续期间被保留。 |
| 令牌过期后应用重启                        | 始终在启动时获取新的令牌——不要在不同启动之间缓存令牌。    |

***

### 使用示例

#### 示例 1：按会话令牌的 LMS 平台

一家企业安全培训平台会在 LMS 登录响应中发放一个 Convai PAT。学员通过身份验证后，会在服务器端生成该令牌，并与 LMS 会话数据一起返回。

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

        ConvaiManager manager = ConvaiManager.ActiveManager;
        if (manager == null) return;

        // 身份与学员记录绑定——长期记忆会跟随学员。
        manager.SetEndUserIdentityProvider(new LmsIdentityProvider(session));

        // PatConvaiManager.Awake() 已通过 FetchTokenFromBackendAsync() 加载了 PAT。
        await manager.ConnectAsync();
    }
}
```

#### 示例 2：按住户轮换令牌的共享自助终端

每位住户登录共享培训自助终端后，都会从后端收到一个新的 PAT，进行模拟交互，然后注销。注销时会显式撤销该令牌，因此它无法再次使用。

```csharp
// KioskSessionManager.cs
using System.Text;
using System.Threading.Tasks;
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 residentSessionBearer)
    {
        _currentToken = await FetchTokenAsync(residentSessionBearer);

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

        await ConvaiManager.ActiveManager.ConnectAsync();
    }

    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
public class LongRunningSessionManager : MonoBehaviour
{
    [SerializeField] private PatConvaiManager _patManager;

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

        // 重新 Awake 时会再次调用 PatConvaiManager.FetchTokenFromBackendAsync()，
        // 或者实现一个公开的 RefreshToken() 方法来调用你的后端端点。
        // 你的后端可以调用 /user/extend-token 或生成一个新令牌——两种方式都可以。

        await ConvaiManager.ActiveManager.ConnectAsync();
    }
}
```

***

### 故障排查

| 症状                          | 可能原因                                                 | 修复                                                                                               |
| --------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| 连接立即失败                      | `apiAuthToken` 为 null——后端获取失败                        | 请在控制台中查看 `令牌获取失败` 日志；验证你的后端端点 URL 和身份验证头。                                                        |
| `401` 连接时 Convai 返回的身份验证错误  | 令牌在之前就已经过期或被撤销 `ConnectAsync()`                      | 务必在连接前立即获取新的令牌——不要在不同会话之间重复使用缓存的令牌。                                                              |
| 连接后会话立即断开                   | `serverUrl` 在以下内容中有误 `ConvaiBootstrapConfigSnapshot` | 设置 `serverUrl` 到 <code class="expression">space.vars.live\_server\_url</code> ——PAT 的作用范围限定于此端点。 |
| `apiAuthToken` 在后端响应中为 null | 请求正文格式错误，或缺少 `CONVAI-API-KEY` 后端调用中的头部               | 请确保正文为 `{}` 并且头部存在。请在后端记录原始响应以检查错误。                                                              |
| 令牌在开发环境可用，但在生产构建中失败         | `ConvaiSettings.asset` API 密钥字段为空，且未获取 PAT           | 请确认 `PatConvaiManager.Awake()` 在以下操作之前运行并完成后端获取： `base.Awake()` 被调用。                             |

***

### 下一步

{% 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:

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

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.
