# Playfab 桥接

本指南将向你展示如何使用 PlayFab 设置游戏，以便在玩家通过 PlayFab 匹配器完成匹配后，自动在 Edgegap 上部署服务器。客户端将通过 PlayFab 玩家数据（Player Data）访问连接到服务器所需的信息，服务器在准备就绪后会更新这些数据。在开始之前，我们预期你：

* 你目前在使用 Playfab；
* 你已经 [创建了](/zh/learn/bian-pai/application-and-versions.md) 与 [部署了](/zh/learn/bian-pai.md) 一个 Edgegap 应用。

你可以在我们的 [GitHub](https://github.com/edgegap/sample-playfab-matchmaker-fishnet)上找到最终示例项目。该示例使用 Unity v2021.3.10 和 Fishnet v3.11.14 作为网络代码进行测试。

### 设置 PlayFab Cloud Script

#### 创建新函数

登录你的 PlayFab 账户，然后在控制台导航到你的游戏总览页面。在 `Automation（自动化）` 选项卡中，创建一个新的 Cloud Script 函数，用于向 Edgegap API 发送请求以部署新服务器。

你需要在请求中传递一个玩家纬度/经度位置列表，这样服务器就能在对所有人来说尽可能最佳的位置进行部署。

你还需要以键值对的形式传递一些自定义环境变量，即：

* 由 `Match ID（匹配 ID）` 由 PlayFab 匹配器生成；
* 一个 PlayFab `Title Secret Key（标题密钥）`，可在你的标题设置中找到；
* 一个 `列表` 包含每位玩家的 `Master Player Account Id（主玩家账户 ID）`.

这些变量将用于更新玩家的 PlayFab `Player Data（玩家数据）` ，其中包含连接到服务器所需的信息。请记录下你用于设置这些变量的键；在此示例中，它们是 `"PLAYFAB_MATCH_ID"`, `"PLAYFAB_TITLE_SECRET"`，以及 `"PLAYFAB_MATCH_PLAYERS_ID_LIST"`.

{% hint style="info" %}
创建此函数的最简单方法是在 `Revisions (Legacy)（修订版-旧版）` 选项卡中部署一个新的修订版，并将以下代码添加进去。请确保先将 `Edgegap_AppName`, `Edgegap_AppVersion`, `Edgegap_ApiToken` 与 `PlayFab_TitleSecret` 顶部的变量更新为你自己的值。

应用名称和版本稍后将在 Edgegap 上容器化游戏服务器时使用。

玩家 ID 列表在此作为一个单一字符串存储，每个 ID 之间用逗号分隔。
{% endhint %}

```javascript
var Edgegap_AppName = "APP_NAME";
var Edgegap_AppVersion = "APP_VERSION";
var Edgegap_ApiToken = "token XXXX"; // 包含关键字 "token"
var PlayFab_TitleSecret = "TITLE_SECRET";

handlers.StartEdgegapDeployment = function (args, context) {
  var QueueName = context.playStreamEvent.Payload.QueueName;
  var MatchId = context.playStreamEvent.Payload.MatchId;

  var GeoIpList = [];
  var PlayerIds = "";

  var MatchMember = multiplayer.GetMatch({
    QueueName: QueueName,
    MatchId: MatchId,
    EscapeObject: false,
    ReturnMemberAttributes: false,
  }).Members;

  MatchMember.forEach((Member) => {
    var Profile = entity.GetProfile({
      Entity: Member.Entity,
    });

    var Location = server.GetPlayerProfile({
      PlayFabId: Profile.Profile.Lineage.MasterPlayerAccountId,
      ProfileConstraints: {
        ShowLocations: true,
      },
    });

    GeoIpList.push({
      ip: "1.2.3.4",
      latitude: Location.PlayerProfile.Locations[0].Latitude,
      longitude: Location.PlayerProfile.Locations[0].Longitude,
    });

    PlayerIds += Profile.Profile.Lineage.MasterPlayerAccountId + ",";
  });

  PlayerIds = PlayerIds.slice(0, -1);

  var response = JSON.parse(
    http.request(
      "https://api.edgegap.com/v1/deploy",
      "post",
      JSON.stringify({
        app_name: Edgegap_AppName,
        version_name: Edgegap_AppVersion,
        geo_ip_list: [...GeoIpList],
        tags: [MatchId.slice(0, 20)],
        env_vars: [
          {
            key: "PLAYFAB_MATCH_ID",
            value: MatchId,
          },
          {
            key: "PLAYFAB_TITLE_SECRET",
            value: TitleSecret,
          },
          {
            key: "PLAYFAB_MATCH_PLAYERS_ID_LIST",
            value: PlayerIds,
          },
        ],
      }),
      "application/json",
      { authorization: Edgegap_ApiToken }
    )
  );

  log.info("response", response);
};
```

{% hint style="info" %}
此 Cloud Script 函数在游戏客户端与 Edgegap API 之间创建一个接口，从而将你的 `Edgegap API 令牌` 与 `PlayFab 标题密钥` 对玩家隐藏。
{% endhint %}

### 自动化规则

仍在 `Automation（自动化）`下，导航到 `Rules（规则）` 选项卡。按以下设置创建一个新规则：

* 将 `Event Type（事件类型）` 转向 `playfab.matchmaking.match_found`;
* 添加一个新的 `Action（操作）`:
  * 将 `Type（类型）` 转向 `Execute Entity Cloud Script（执行实体 Cloud Script）`;
  * 将 `Cloud Script Function（函数）` 转向 `StartEdgegapDeployment`.

<figure><img src="/files/d021bba0c2fbfc265825530790e92b278f2f1f38" alt=""><figcaption></figcaption></figure>

### 匹配器队列与玩家

在 `Multiplayer（多人）` 选项卡下的 `Matchmaking（匹配）`中，确保有可用的队列，否则创建一个新队列。为了快速测试，将其设置为简单地匹配任意两个玩家。

你需要在你的 Playfab 标题下注册足够的玩家以满足匹配器规则，因此此处只需两名玩家即可。你可以在 `Players（玩家）` 选项卡下创建新的玩家（如有需要）。在此示例项目中，请务必记录这些玩家的 `Custom ID（自定义 ID）` 与 `Title Player Account ID（标题玩家账户 ID）` 以便稍后测试。

### 编辑游戏

#### 服务器

在 Unity 编辑器中打开游戏示例后，我们在 `BattleScene` 中（位于 `Assets/SpaceEdge/Scenes`下）添加了一个新的空 gameObject。然后我们将一个名为 `PlayerDataUpdateService` 的新 C# 脚本挂载到它上面。它包含一个在服务器准备就绪后执行的回调函数。

我们还创建了第二个脚本，名为 `PlayfabRestApiService`，其中包含需要发送到 PlayFab 不同 API 的各种请求。

在 `PlayerDataUpdateService.cs`中，我们获取部署时注入的部分环境变量：

* 自定义变量：
  * `PLAYFAB_MATCH_ID`;
  * `PLAYFAB_TITLE_SECRET`;
  * `PLAYFAB_MATCH_PLAYERS_ID_LIST`
* Edgegap 默认变量：
  * `ARBITRIUM_PUBLIC_IP`;
  * `ARBITRIUM_PORTS_MAPPING`;

在 `ARBITRIUM_PORTS_MAPPING`，我们需要获取特定的外部 `端口` 值，以便客户端能够连接到服务器；在此示例中，它的名称为 `"Game Port"`.

使用 `PLAYFAB_TITLE_SECRET` 作为认证，我们向 PlayFab Server API 发送请求以更新 `Player Data（玩家数据）` 中每个对应 ID 的 `PLAYFAB_MATCH_PLAYERS_ID_LIST` ，使用以下键值对：

* 键： `PLAYFAB_MATCH_ID`;
* 值： `"{ARBITRIUM_PUBLIC_IP}:{port}"`;

#### PlayerDataUpdateService

```cs
public class PlayerDataUpdateService : NetworkBehaviour
{
    private PlayfabRestApiService _playfabService;

    [SerializeField] private string KeyMatchId = "PLAYFAB_MATCH_ID";
    [SerializeField] private string KeyTitleSecret = "PLAYFAB_TITLE_SECRET";
    [SerializeField] private string KeyPlayerIds = "PLAYFAB_MATCH_PLAYERS_ID_LIST";
    [SerializeField] private string ServerPortName = "Game Port";

    private string matchID;
    private string titleSecretKey;
    private string[] playerIdList;

    public override void OnStartServer()
    {
        _playfabService = FindObjectOfType<PlayfabRestApiService>();

        matchID = Environment.GetEnvironmentVariable(KeyMatchId);
        titleSecretKey = Environment.GetEnvironmentVariable(KeyTitleSecret);
        playerIdList = Environment.GetEnvironmentVariable(KeyPlayerIds)?.Split(',');

        string address = Environment.GetEnvironmentVariable("ARBITRIUM_PUBLIC_IP");
        string portsMapping = Environment.GetEnvironmentVariable("ARBITRIUM_PORTS_MAPPING");

        if (!string.IsNullOrEmpty(portsMapping))
        {
            int port = JSON.ParseString(portsMapping).GetJSON("ports").GetJSON(ServerPortName).GetInt("external");

            foreach (string playerId in playerIdList)
            {
                if (!string.IsNullOrEmpty(playerId))
                {
                    _playfabService.UpdatePlayerData(titleSecretKey, playerId, matchID, $"{address}:{port}");
                }
            }
        }
        else
        {
            Debug.Log("Port Mapping Unavailable");
        }
    }

    ...
}
```

#### PlayfabRestApiService

```cs
public class PlayfabRestApiService : MonoBehaviour
{
    ...

    [SerializeField] private string TitleID = "TITLE_ID";

    private const string TypeHeaderValue = "application/json";
    private readonly HttpClient _playfabHttpClient = new();
    private HttpResponseMessage _request;
    private StringContent _requestData;
    private string _response;

    ...

    public async void UpdatePlayerData(string secretKey, string playerID, string key, string value)
    {
        if (!_playfabHttpClient.DefaultRequestHeaders.Contains("X-SecretKey"))
        {
            _playfabHttpClient.DefaultRequestHeaders.Add("X-SecretKey", secretKey);
        }

        var dataProperty = new JSON();
        dataProperty.Add(key, value);

        var requestJson = new JSON();
        requestJson.Add("PlayFabId", playerID);
        requestJson.Add("Data", dataProperty);

        _requestData = new StringContent(requestJson.CreateString(), Encoding.UTF8, TypeHeaderValue);
        _request = await _playfabHttpClient.PostAsync($"https://{TitleID}.playfabapi.com/Server/UpdateUserData", _requestData);
        _response = await _request.Content.ReadAsStringAsync();

        if (!_request.IsSuccessStatusCode)
        {
            Debug.Log($"Could Not Update PlayerData of ID {playerID}, Error {(int)_request.StatusCode} With Message: \n{_response}");
        }
    }
}
```

#### 客户端

客户端的设置将在 `MainMenu` 场景中进行，位于 `Assets/SpaceEdge/Scenes`下。我们创建了一个名为 `MainMenuService` 的新脚本来管理该场景的 UI 并向玩家显示各种消息，并向 `PlayfabRestApiService.cs`中添加了新函数。我们也将该 `PlayfabRestApiService` 脚本挂载到了该场景中的一个新 gameObject 上。

玩家需要做的第一件事是使用 PlayFab 登录，这将允许他们向 PlayFab 的 Client 和 Multiplayer API 发送额外的请求。

{% hint style="info" %}
我们为简化使用 `LoginWithCustomID` 端点，不过根据实际情况（例如移动游戏）有更合适的登录实现方式。
{% endhint %}

当玩家登录并且 `X-Authorization` 与 `X-EntityToken` 这两个请求头为后续请求正确设置后，他们可以使用 `Start Game（开始游戏）` UI 按钮创建一个新的匹配票据（ticket）。这将向 PlayFab 的多人 API 发送请求并存储 `ticket ID（票据 ID）` 以供后续使用。

之后，游戏客户端会每隔几秒定期检查该票据的状态，直到找到匹配或票据被取消。一旦找到匹配， `match ID（匹配 ID）` 会被存为一个变量。

随后，客户端会定期向 PlayFab 的 Client API 发送请求以获取玩家的标题 `Player Data（玩家数据）`，使用 `match ID（匹配 ID）` 作为需要检索的特定数据的键。一旦找到数据，它会在网络代码的 `地址` 和外部 `端口` 值中设置 `传输（transport）` 组件，然后连接到服务器。

当客户端正确连接并启动后，会发送请求从该玩家的 PlayFab `Player Data（玩家数据）` 中移除连接信息，这要归功于一个回调函数。

{% hint style="info" %}
必须确保这些键值对被删除，最好在部署终止之前。这是为了避免将 `Player Data（玩家数据）` 弄得充斥着太多不必要的条目。
{% endhint %}

#### MainMenuService

```cs
public class MainMenuService : MonoBehaviour
{
    [SerializeField] private Button startGameButton;
    [SerializeField] private TMP_InputField playerNameInput;
    [SerializeField] private TMP_Text serverStatus;

    private PlayfabRestApiService _playfabService;

    private void Start()
    {
        if (!InstanceFinder.ServerManager.GetStartOnHeadless())
        {
            _playfabService = FindObjectOfType<PlayfabRestApiService>();
            _playfabService.OnStatusUpdate += ServerStatusUpdate;
            _playfabService.OnUpdateButton += UpdateButtonState;

            startGameButton.onClick.AddListener(StartGame);
            playerNameInput.onValueChanged.AddListener(NameChanged);
            if (PlayerPrefs.HasKey("PlayerName"))
                playerNameInput.text = PlayerPrefs.GetString("PlayerName");

            _playfabService.LoginPlayer();
        }
    }

    private void OnDestroy()
    {
        if (!InstanceFinder.ServerManager.GetStartOnHeadless())
        {
            _playfabService.OnStatusUpdate -= ServerStatusUpdate;
            _playfabService.OnUpdateButton -= UpdateButtonState;
        }
    }

    private void StartGame()
    {
        startGameButton.interactable = false;
        serverStatus.text = "";
        _playfabService.CreateTicket();
    }

    private void ServerStatusUpdate(string status, bool isError)
    {
        serverStatus.text += "\n" + status;
        serverStatus.color = isError ? Color.red : Color.green;
        Debug.Log(status);
    }

    private void UpdateButtonState(bool state)
    {
        startGameButton.interactable = state;
        serverStatus.text = "";
    }

    private void NameChanged(string text) => PlayerPrefs.SetString("PlayerName", text);
}
```

#### PlayfabRestApiService

```cs
public class PlayfabRestApiService : MonoBehaviour
{
    [SerializeField] private string PlayerCustomID = "PLAYER_CUSTOM_ID";
    [SerializeField] private string PlayerTitleID = "PLAYER_TITLE_ID";
    [SerializeField] private string TitleID = "TITLE_ID";
    [SerializeField] private string QueueName = "QUEUE_NAME";

    private const string TypeHeaderValue = "application/json";
    private readonly HttpClient _playfabHttpClient = new();
    private HttpResponseMessage _request;
    private StringContent _requestData;
    private string _response;
    private string _ticketID = "";
    private string _matchID = "";

    public Action<string, bool> OnStatusUpdate;
    public Action<bool> OnUpdateButton;

    private static PlayfabRestApiService _instance = null;

    private void Awake()
    {
        _playfabHttpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(TypeHeaderValue));

        if (_instance is null)
        {
            DontDestroyOnLoad(this);
            _instance = this;
        }
        else
        {
            Destroy(this);
        }
    }

    public async void LoginPlayer()
    {
        var requestJson = new JSON();
        requestJson.Add("CustomID", PlayerCustomID);
        requestJson.Add("CreateAccount", false);
        requestJson.Add("TitleID", TitleID);
        _requestData = new StringContent(requestJson.CreateString(), Encoding.UTF8, TypeHeaderValue);

        _request = await _playfabHttpClient.PostAsync($"https://{TitleID}.playfabapi.com/Client/LoginWithCustomID", _requestData);
        _response = await _request.Content.ReadAsStringAsync();

        if (_request.IsSuccessStatusCode)
        {
            var responseJson = JSON.ParseString(_response);

            string token = responseJson.GetJSON("data").GetJSON("EntityToken").GetString("EntityToken");
            _playfabHttpClient.DefaultRequestHeaders.Add("X-EntityToken", token);

            string sessionTicket = responseJson.GetJSON("data").GetString("SessionTicket");
            _playfabHttpClient.DefaultRequestHeaders.Add("X-Authorization", sessionTicket);

            OnUpdateButton?.Invoke(true);
            OnStatusUpdate?.Invoke("Login Successful", false);
        }
        else
        {
            OnStatusUpdate?.Invoke($"Unable To Login Player, Error {(int)_request.StatusCode} With Message: \n{_response}", true);
        }
    }

    public async void CreateTicket()
    {
        var requestJson = new JSON();

        var entityProperty = new JSON();
        entityProperty.Add("Id", PlayerTitleID);
        entityProperty.Add("Type", "title_player_account");

        var creatorProperty = new JSON();
        creatorProperty.Add("Entity", entityProperty);

        requestJson.Add("Creator", creatorProperty);
        requestJson.Add("GiveUpAfterSeconds", 90);
        requestJson.Add("QueueName", QueueName);
        _requestData = new StringContent(requestJson.CreateString(), Encoding.UTF8, TypeHeaderValue);

        _request = await _playfabHttpClient.PostAsync($"https://{TitleID}.playfabapi.com/Match/CreateMatchmakingTicket", _requestData);
        _response = await _request.Content.ReadAsStringAsync();

        if (_request.IsSuccessStatusCode)
        {
            var responseJson = JSON.ParseString(_response);
            _ticketID = responseJson.GetJSON("data").GetString("TicketId");

            OnStatusUpdate?.Invoke($"Ticket Created, ID #{_ticketID}", false);

            GetMatchIdAttempt();
        }
        else
        {
            OnUpdateButton?.Invoke(true);
            OnStatusUpdate?.Invoke($"Unable To Create Ticket, Error {(int)_request.StatusCode} With Message: \n{_response}", true);
        }
    }

    private async void GetMatchIdAttempt()
    {
        bool isMatchFound = false;
        var requestJson = new JSON();
        requestJson.Add("EscapeObject", true);
        requestJson.Add("QueueName", QueueName);
        requestJson.Add("TicketId", _ticketID);
        _requestData = new StringContent(requestJson.CreateString(), Encoding.UTF8, TypeHeaderValue);

        while (!isMatchFound)
        {
            _request = await _playfabHttpClient.PostAsync($"https://{TitleID}.playfabapi.com/Match/GetMatchmakingTicket", _requestData);
            _response = await _request.Content.ReadAsStringAsync();

            if (_request.IsSuccessStatusCode)
            {
                var responseJson = JSON.ParseString(_response);
                string ticketStatus = responseJson.GetJSON("data").GetString("Status");
                isMatchFound = ticketStatus == "Matched";

                if (isMatchFound)
                {
                    _matchID = responseJson.GetJSON("data").GetString("MatchId");
                    OnStatusUpdate?.Invoke($"Ticket Has Been Matched, ID #{_matchID}", false);

                    StartConnectionAttempt();
                }
                else if (ticketStatus == "Canceled")
                {
                    isMatchFound = true;
                    string reason = responseJson.GetJSON("data").GetString("CancellationReason");

                    OnUpdateButton?.Invoke(true);
                    OnStatusUpdate?.Invoke($"Ticket Has Been Cancelled Due To {reason}", false);
                }
                else
                {
                    OnStatusUpdate?.Invoke("Match Not Found, Retrying In 10 Seconds...", false);
                    await Task.Delay(10000);
                }

            }
            else
            {
                isMatchFound = true;
                OnUpdateButton?.Invoke(true);
                OnStatusUpdate?.Invoke($"Could Not Get Ticket Info, Error {(int)_request.StatusCode} With Message: \n{_response}", true);
            }
        }
    }

    private async void StartConnectionAttempt()
    {
        bool gotConnectionInfo = false;

        var requestJson = new JSON();
        requestJson.Add("Keys", new List<string> { _matchID });

        _requestData = new StringContent(requestJson.CreateString(), Encoding.UTF8, TypeHeaderValue);

        while (!gotConnectionInfo)
        {
            _request = await _playfabHttpClient.PostAsync($"https://{TitleID}.playfabapi.com/Client/GetUserData", _requestData);
            _response = await _request.Content.ReadAsStringAsync();

            if (_request.IsSuccessStatusCode)
            {
                var responseJson = JSON.ParseString(_response);
                var dataObject = responseJson.GetJSON("data").GetJSON("Data");

                if (dataObject.Count > 0)
                {
                    gotConnectionInfo = true;
                    string connectionInfo = dataObject.GetJSON(_matchID).GetString("Value");
                    string serverAddress = connectionInfo.Split(':')[0];

                    if (ushort.TryParse(connectionInfo.Split(':')[1], out ushort serverPort))
                    {
                        InstanceFinder.TransportManager.Transport.SetPort(serverPort);
                        InstanceFinder.TransportManager.Transport.SetClientAddress(serverAddress);
                        Debug.Log($"Connecting To Server Using Port {serverPort} And IP {serverAddress}");
                        InstanceFinder.ClientManager.StartConnection();
                    }
                    else
                    {
                        OnUpdateButton?.Invoke(true);
                        OnStatusUpdate?.Invoke($"Error While Parsing Server Port Value", true);
                    }
                }
                else
                {
                    OnStatusUpdate?.Invoke("No Connection Data Found, Retrying In 10 Seconds...", false);
                    await Task.Delay(10000);
                }
            }
            else
            {
                gotConnectionInfo = true;
                OnUpdateButton?.Invoke(true);
                OnStatusUpdate?.Invoke($"Could Not Get Player Data, Error {(int)_request.StatusCode} With Message: \n{_response}", true);
            }
        }
    }

    public async void RemovePlayerData()
    {
        var requestJson = new JSON();
        requestJson.Add("KeysToRemove", new string[] { _matchID });

        _requestData = new StringContent(requestJson.CreateString(), Encoding.UTF8, TypeHeaderValue);
        _request = await _playfabHttpClient.PostAsync($"https://{TitleID}.playfabapi.com/Client/UpdateUserData", _requestData);
        _response = await _request.Content.ReadAsStringAsync();

        if (!_request.IsSuccessStatusCode)
        {
            Debug.Log($"Could Not Update PlayerData, Error {(int)_request.StatusCode} With Message: \n{_response}");
        }
    }

    ...
}
```

#### PlayerDataUpdateService

```cs
public class PlayerDataUpdateService : NetworkBehaviour
{
    private PlayfabRestApiService _playfabService;

    ...

    public override void OnStartClient()
    {
        _playfabService = FindObjectOfType<PlayfabRestApiService>();
        _playfabService.RemovePlayerData();
    }
}
```

### 构建游戏服务器与容器化

在对服务器构建进行容器化之前，确保启用 `Start On Headless（无头启动）` 选项于该 `NetworkManager` gameObject。

通过 `Tools/Edgegap Hosting` 工具栏菜单打开 Edgegap 插件。验证你的 `Edgegap API Token（Edgegap API 令牌）` ，并为游戏创建或加载一个应用。确保 `端口` 值与该 `NetworkManager` gameObject 的 Tugboat 组件中的值匹配。选择 `UDP` 协议，然后输入一个 `New Version Tag（新版本标签）`。请确保应用名称和版本与设置 Cloud Script 时使用的值一致。

完成正确设置后，点击 `Build and Push（构建并推送）`，这将自动容器化你的游戏服务器，并在短暂等待后在 Edgegap 控制台上创建一个新的应用版本。

{% hint style="info" %}
创建应用版本时，插件将默认把端口命名为 \`"Game Port"\`。

如果你选择手动容器化并创建应用版本，而不是使用插件，请确保在选择要使用的端口时使用该名称。
{% endhint %}

### 测试

首先确保禁用 `Start On Headless（无头启动）` 选项于该 `NetworkManager` gameObject。将 `PlayfabRestApiService` 脚本中的 `Custom ID（自定义 ID）` 与 `Title ID（标题 ID）` 设置为你的第一个玩家的值，并设置你的游戏的 `Title ID（标题 ID）` 与 `Queue Name（队列名称）`。在 `Build Settings（构建设置）`中，使用这些设置创建一个新的客户端构建。

构建完成后，将 `Custom ID（自定义 ID）` 与 `Title ID（标题 ID）` 值更改为你的第二个玩家的值。你也可以创建一个单独的客户端构建，尽管在 Unity 编辑器的运行模式下也同样可行。

如果两个客户端在启动时都正确登录，点击两个实例的 `Start Game（开始游戏）` 按钮。稍候片刻，它们将通过 PlayFab 的匹配器匹配，然后在服务器完成设置后连接到 Edgegap 上的同一部署。


---

# Agent Instructions: 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.edgegap.com/zh/docs/tools-and-integrations/playfab-bridge.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.
