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)
我尝试过的:
- 正在创建一个新的 API 密钥
- 将脚本所有者添加到 GCP 项目(根据公司设置,这是一个通用 Google 帐户)
- 在插件的 GCP 项目而不是旧项目中启用选择器 API 并生成新密钥
API键有以下设置:
- 应用限制:HTTP referrers
- 网站限制:
- *.google.com
- *.googleusercontent.com
- 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.
参考:
我有一个使用 Google 选择器 API 的脚本。在我测试它时,它是 运行 完美的,直到我将它发布为私人插件。从那时起,脚本 getOAuthToken 失败并出现以下(极其无用的)错误:
Exception: We're sorry, a server error occurred. Please wait a bit and try again. at getOAuthToken(Code:37:12)
我尝试过的:
- 正在创建一个新的 API 密钥
- 将脚本所有者添加到 GCP 项目(根据公司设置,这是一个通用 Google 帐户)
- 在插件的 GCP 项目而不是旧项目中启用选择器 API 并生成新密钥
API键有以下设置:
- 应用限制:HTTP referrers
- 网站限制:
- *.google.com
- *.googleusercontent.com
- 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.