Google Picker API getOAuthToken() 在作为私有插件发布后抛出服务器错误

Google Picker API getOAuthToken() throws server error after being published as private add-on

我有一个使用 Google 选择器 API 的脚本。在我测试它时,它是 运行 完美的,直到我将它发布为私人插件。从那时起,脚本 getOAuthToken 失败并出现以下(极其无用的)错误:

Exception: We're sorry, a server error occurred. Please wait a bit and try again. at getOAuthToken(Code:37:12)

我尝试过的:

  1. 正在创建一个新的 API 密钥
  2. 将脚本所有者添加到 GCP 项目(根据公司设置,这是一个通用 Google 帐户)
  3. 在插件的 GCP 项目而不是旧项目中启用选择器 API 并生成新密钥

API键有以下设置:

这些设置在发布之前一直有效。

Google 选择器的代码也在下面。它基于 Google API 文档中的 boiler plate 并且曾经按原样工作:

<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">
  <script>

    const pickFileType = '<?= fileType ?>';

    // IMPORTANT: Replace the value for DEVELOPER_KEY with the API key obtained
    // from the Google Developers Console.
    var DEVELOPER_KEY = 'intentionally removed';
    var DIALOG_DIMENSIONS = {width: 900, height: 500};
    var pickerApiLoaded = false;

    /**
     * Loads the Google Picker API.
     */
    function onApiLoad() {
      gapi.load('picker', {'callback': function() {
        pickerApiLoaded = true;
      }});
     }

    /**
     * Gets the user's OAuth 2.0 access token from the server-side script so that
     * it can be passed to Picker. This technique keeps Picker from needing to
     * show its own authorization dialog, but is only possible if the OAuth scope
     * that Picker needs is available in Apps Script. Otherwise, your Picker code
     * will need to declare its own OAuth scopes.
     */
    function getOAuthToken() {
      google.script.run.withSuccessHandler(createPicker)
          .withFailureHandler(showError).getOAuthToken();
    }

    /**
     * Creates a Picker that can access the user's spreadsheets. This function
     * uses advanced options to hide the Picker's left navigation panel and
     * default title bar.
     *
     * @param {string} token An OAuth 2.0 access token that lets Picker access the
     *     file type specified in the addView call.
     */
    function createPicker(token) {
      if (pickerApiLoaded && token) {
        const docsUploadView = new google.picker.DocsUploadView();
        docsUploadView.setIncludeFolders(true);
        
        const viewId = pickFileType === 'folder' ?
          google.picker.ViewId.FOLDERS : google.picker.ViewId.DOCUMENTS;
        
        const drivesView = new google.picker.DocsView(viewId);
        drivesView.setEnableDrives(true);
        drivesView.setIncludeFolders(true);
        if (pickFileType === 'folder') drivesView.setSelectFolderEnabled(true);
        
        const driveView = new google.picker.DocsView(viewId);
        driveView.setSelectFolderEnabled(true);
        driveView.setParent('root');
        if (pickFileType === 'folder') driveView.setIncludeFolders(true);

        console.log(`viewId = ${viewId}`);

        // const docsViewId = new google.picker.ViewGroup(google.picker.viewId.DOCS)
        //   .addView(viewId);

        var picker = new google.picker.PickerBuilder()
            // Instruct Picker to display only spreadsheets in Drive. For other
            // .addViewGroup(docsViewId)
            .addView(driveView)
            .addView(drivesView)
            // .addView(viewId)
            // .addView(docsUploadView)
            // Hide the navigation panel so that Picker fills more of the dialog.
            // .enableFeature(google.picker.Feature.NAV_HIDDEN)
            // .enableFeature(google.picker.Feature.MULTISELECT_ENABLED)
            .enableFeature(google.picker.Feature.SUPPORT_DRIVES)
            // Hide the title bar since an Apps Script dialog already has a title.
            .hideTitleBar()
            .setOAuthToken(token)
            .setDeveloperKey(DEVELOPER_KEY)
            .setCallback(pickerCallback)
            .setOrigin(google.script.host.origin)
            // Instruct Picker to fill the dialog, minus 2 pixels for the border.
            .setSize(DIALOG_DIMENSIONS.width - 2,
                DIALOG_DIMENSIONS.height - 2)
            .build();
        picker.setVisible(true);
      } else {
        showError('Unable to load the file picker.');
      }
    }

    /**
     * A callback function that extracts the chosen document's metadata from the
     * response object. For details on the response object, see
     * https://developers.google.com/picker/docs/result
     *
     * @param {object} data The response object.
     */
    function pickerCallback(data) {
      let selectedId;
      console.log(data);
      var action = data[google.picker.Response.ACTION];
      if (action == google.picker.Action.PICKED) {
        // const array = [['Nom', 'ID', 'URL']];
        const docs = data[google.picker.Response.DOCUMENTS];
        docs.forEach(doc => {
          var id = doc[google.picker.Document.ID];
          selectedId = id;
          // var url = doc[google.picker.Document.URL];
          // var title = doc[google.picker.Document.NAME];
          // array.push([title, id, url]);
        });

        google.script.run.withSuccessHandler(() => {
          google.script.run.showFront(true);
        }).writeVar(pickFileType, selectedId);
        
      } else if (action == google.picker.Action.CANCEL) {
        google.script.run.showFront(true);
      }
    }

    /**
     * Displays an error message within the #result element.
     *
     * @param {string} message The error message to display.
     */
    function showError(message) {
      document.getElementById('result').innerHTML = 'Error: ' + message;
    }
  </script>
</head>
<body>
  <div>
    <p id='result'></p>
  </div>
  <script src="https://apis.google.com/js/api.js?onload=onApiLoad"></script>
  <script>
    window.onload = getOAuthToken;
  </script>
</body>
</html>

编辑:这是服务器端 getOAuthToken 的样子。我把评论留在里面了。

/**
 * Gets the user's OAuth 2.0 access token so that it can be passed to Picker.
 * This technique keeps Picker from needing to show its own authorization
 * dialog, but is only possible if the OAuth scope that Picker needs is
 * available in Apps Script. In this case, the function includes an unused call
 * to a DriveApp method to ensure that Apps Script requests access to all files
 * in the user's Drive.
 *
 * @return {string} The user's OAuth 2.0 access token.
 */
function getOAuthToken() {
  DriveApp.getRootFolder();
  return ScriptApp.getOAuthToken();
}

编辑 2

我想我越来越接近理解了。每次我尝试访问云端硬盘应用程序时,脚本现在都会抛出这些类型的错误。当我做 DriveApp.getFolderById(id)

时我得到同样的错误

Unexpected error while getting the method or property getFileById on object DriveApp when I do DriveApp.getFileById(id).

我已将作用域添加到我的清单中,但它仍然无济于事。这是清单:

{
  "timeZone": "Europe/Paris",
  "dependencies": {
  },
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": [
    "https://www.googleapis.com/auth/drive",
    "https://www.googleapis.com/auth/spreadsheets",
    "https://www.googleapis.com/auth/script.container.ui",
    "https://www.googleapis.com/auth/userinfo.email",
    "https://www.googleapis.com/auth/spreadsheets"
    ]
}

如果将 default Cloud Platform project 用于您的 Apps 脚本,则在保存脚本项目时,脚本使用的任何 API 都会自动启用。

当你 switch to a standard GCP project 时就不是这样了。在这种情况下,API 不会自动启用,您必须在 GCP 项目上手动启用它们:

Often an Apps Script application needs access to another Google API. This requires you to enable the API in the corresponding GCP project.

根据文档,这仅适用于 Advanced Services,但它至少也适用于某些标准服务。看到这个问题:

具体来说this comment:

There are mentions about enabling APIs for advanced services here. But not for the standard services, I notified this to the documentation team.

参考: