1

我有一个使用 Google Picker API 的脚本。在我对其进行测试时,它运行良好,直到我将其作为私人插件发布。从那时起,脚本 getOAuthToken 失败并出现以下(非常无用)错误:

例外:很抱歉,发生服务器错误。请稍等,然后重试。在 getOAuthToken(代码:37:12)

我试过的:

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

API 密钥具有以下设置:

  • 应用程序限制:HTTP 引荐来源网址
  • 网站限制:
    • *.google.com
    • *.googleusercontent.com
  • API 限制:不限制密钥

这些设置在发布之前就可以使用。

谷歌选择器的代码也在下面。它基于Google API 文档中的样板,并按原样工作:

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

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

当我在对象 DriveApp 上获取方法或属性 getFileById 时出现意外错误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"
    ]
}
4

1 回答 1

2

为您的 Apps 脚本使用默认 Cloud Platform 项目时,脚本使用的任何 API 在保存脚本项目时都会自动启用。

当您切换到标准 GCP 项目时,情况并非如此。在这种情况下,API 不会自动启用,您必须在 GCP 项目中手动启用它们:

Apps 脚本应用程序通常需要访问另一个 Google API。这需要您在相应的 GCP 项目中启用 API。

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

特别是这条评论

这里提到了为高级服务启用 API 。但不适用于标准服务,我将此通知给了文档团队。

参考:

于 2020-12-02T14:54:17.610 回答