/**
* Responds to a MESSAGE event in Google Chat.
*
* This app only responds to a slash command with the ID 1 ("/closeIncident").
* It will respond to any other message with a simple "Hello" text message.
*
* @param {Object} event the event object from Google Chat
*/
function onMessage(event) {
if (event.message.slashCommand) {
return processSlashCommand_(event);
}
return { "text": "Hello from Incident Response app!" };
}
/**
* Responds to a CARD_CLICKED event in Google Chat.
*
* This app only responds to one kind of dialog (Close Incident).
*
* @param {Object} event the event object from Google Chat
*/
function onCardClick(event) {
if (event.isDialogEvent) {
if (event.dialogEventType == 'SUBMIT_DIALOG') {
return processSubmitDialog_(event);
}
return {
actionResponse: {
type: "DIALOG",
dialogAction: {
actionStatus: "OK"
}
}
};
}
}
/**
* Responds to a MESSAGE event with a Slash command in Google Chat.
*
* This app only responds to a slash command with the ID 1 ("/closeIncident")
* by returning a Dialog.
*
* @param {Object} event the event object from Google Chat
*/
function processSlashCommand_(event) {
if (event.message.slashCommand.commandId != CLOSE_INCIDENT_COMMAND_ID) {
return {
"text": "Command not recognized. Use the command `/closeIncident` to close the incident managed by this space."
};
}
const sections = [
{
header: "Close Incident",
widgets: [
{
textInput: {
label: "Please describe the incident resolution",
type: "MULTIPLE_LINE",
name: "description"
}
},
{
buttonList: {
buttons: [
{
text: "Close Incident",
onClick: {
action: {
function: "closeIncident"
}
}
}
]
}
}
]
}
];
return {
actionResponse: {
type: "DIALOG",
dialogAction: {
dialog: {
body: {
sections,
}
}
}
}
};
}
/**
* Responds to a CARD_CLICKED event with a Dialog submission in Google Chat.
*
* This app only responds to one kind of dialog (Close Incident).
* It creates a Doc with a summary of the incident information and posts a message
* to the space with a link to the Doc.
*
* @param {Object} event the event object from Google Chat
*/
function processSubmitDialog_(event) {
const resolution = event.common.formInputs.description[""].stringInputs.value[0];
const chatHistory = concatenateAllSpaceMessages_(event.space.name);
const chatSummary = summarizeChatHistory_(chatHistory);
const docUrl = createDoc_(event.space.displayName, resolution, chatHistory, chatSummary);
return {
actionResponse: {
type: "NEW_MESSAGE",
},
text: `Incident closed with the following resolution: ${resolution}\n\nHere is the automatically generated post-mortem:\n${docUrl}`
};
}
/**
* Lists all the messages in the Chat space, then concatenate all of them into
* a single text containing the full Chat history.
*
* For simplicity for this demo, it only fetches the first 100 messages.
*
* Messages with slash commands are filtered out, so the returned history will
* contain only the conversations between users and not app command invocations.
*
* @return {string} a text containing all the messages in the space in the format:
* Sender's name: Message
*/
function concatenateAllSpaceMessages_(spaceName) {
// Call Chat API method spaces.messages.list
const response = Chat.Spaces.Messages.list(spaceName, { 'pageSize': 100 });
const messages = response.messages;
// Fetch the display names of the message senders and returns a text
// concatenating all the messages.
let userMap = new Map();
return messages
.filter(message => message.slashCommand === undefined)
.map(message => `${getUserDisplayName_(userMap, message.sender.name)}: ${message.text}`)
.join('\n');
}
/**
* Obtains the display name of a user by using the Admin Directory API.
*
* The fetched display name is cached in the provided map, so we only call the API
* once per user.
*
* If the user does not have a display name, then the full name is used.
*
* @param {Map} userMap a map containing the display names previously fetched
* @param {string} userName the resource name of the user
* @return {string} the user's display name
*/
function getUserDisplayName_(userMap, userName) {
if (userMap.has(userName)) {
return userMap.get(userName);
}
let displayName = 'Unknown User';
try {
const user = AdminDirectory.Users.get(
userName.replace("users/", ""),
{ projection: 'BASIC', viewType: 'domain_public' });
displayName = user.name.displayName ? user.name.displayName : user.name.fullName;
} catch (e) {
// Ignore error if the API call fails (for example, because it's an
// out-of-domain user or Chat app)) and just use 'Unknown User'.
}
userMap.set(userName, displayName);
return displayName;
}
/**
* Creates a space in Google Chat with the provided title and members, and posts an
* initial message to it.
*
* @param {Object} formData the data submitted by the user. It should contain the fields
* title, description, and users.
* @return {string} the resource name of the new space.
*/
function createChatSpace(formData) {
const users = formData.users.trim().length > 0 ? formData.users.split(',') : [];
const spaceName = setUpSpace_(formData.title, users);
addAppToSpace_(spaceName);
createMessage_(spaceName, formData.description);
return spaceName;
}
/**
* Creates a space in Google Chat with the provided display name and members.
*
* @return {string} the resource name of the new space.
*/
function setUpSpace_(displayName, users) {
const memberships = users.map(email => ({
member: {
name: `users/${email}`,
type: "HUMAN"
}
}));
const request = {
space: {
displayName: displayName,
spaceType: "SPACE",
externalUserAllowed: true
},
memberships: memberships
};
// Call Chat API method spaces.setup
const space = Chat.Spaces.setup(request);
return space.name;
}
/**
* Adds this Chat app to the space.
*
* @return {string} the resource name of the new membership.
*/
function addAppToSpace_(spaceName) {
const request = {
member: {
name: "users/app",
type: "BOT"
}
};
// Call Chat API method spaces.members.create
const membership = Chat.Spaces.Members.create(request, spaceName);
return membership.name;
}
/**
* Posts a text message to the space on behalf of the user.
*
* @return {string} the resource name of the new message.
*/
function createMessage_(spaceName, text) {
const request = {
text: text
};
// Call Chat API method spaces.messages.create
const message = Chat.Spaces.Messages.create(request, spaceName);
return message.name;
}
DocsApi.gs
调用 Google 文档 API 以在
用户的 Google 云端硬盘,并撰写事件信息摘要,
(在 VertexAiApi.gs 中创建)到文档。
/**
* Creates a Doc in the user's Google Drive and writes a summary of the incident information to it.
*
* @param {string} title The title of the incident
* @param {string} resolution Incident resolution described by the user
* @param {string} chatHistory The whole Chat history be included in the document
* @param {string} chatSummary A summary of the Chat conversation to be included in the document
* @return {string} the URL of the created Doc
*/
function createDoc_(title, resolution, chatHistory, chatSummary) {
let doc = DocumentApp.create(title);
let body = doc.getBody();
body.appendParagraph(`Post-Mortem: ${title}`).setHeading(DocumentApp.ParagraphHeading.TITLE);
body.appendParagraph("Resolution").setHeading(DocumentApp.ParagraphHeading.HEADING1);
body.appendParagraph(resolution);
body.appendParagraph("Summary of the conversation").setHeading(DocumentApp.ParagraphHeading.HEADING1);
body.appendParagraph(chatSummary);
body.appendParagraph("Full Chat history").setHeading(DocumentApp.ParagraphHeading.HEADING1);
body.appendParagraph(chatHistory);
return doc.getUrl();
}
/**
* Summarizes a Chat conversation using the Vertex AI text prediction API.
*
* @param {string} chatHistory The Chat history that will be summarized.
* @return {string} The content from the text prediction response.
*/
function summarizeChatHistory_(chatHistory) {
const prompt =
"Summarize the following conversation between Engineers resolving an incident"
+ " in a few sentences. Use only the information from the conversation.\n\n"
+ chatHistory;
const request = {
instances: [
{ prompt: prompt }
],
parameters: {
temperature: 0.2,
maxOutputTokens: 256,
topK: 40,
topP: 0.95
}
}
const fetchOptions = {
method: 'POST',
headers: { Authorization: 'Bearer ' + ScriptApp.getOAuthToken() },
contentType: 'application/json',
payload: JSON.stringify(request)
}
const response = UrlFetchApp.fetch(
`https://${VERTEX_AI_LOCATION_ID}-aiplatform.googleapis.com/v1`
+ `/projects/${PROJECT_ID}/locations/${VERTEX_AI_LOCATION_ID}`
+ "/publishers/google/models/text-bison:predict",
fetchOptions);
const payload = JSON.parse(response.getContentText());
return payload.predictions[0].content;
}
/**
* Serves the web page from Index.html.
*/
function doGet() {
return HtmlService
.createTemplateFromFile('Index')
.evaluate();
}
/**
* Serves the web content from the specified filename.
*/
function include(filename) {
return HtmlService
.createHtmlOutputFromFile(filename)
.getContent();
}
/**
* Returns the email address of the user running the script.
*/
function getUserEmail() {
return Session.getActiveUser().getEmail();
}
突发事件响应应用使用用户凭据进行身份验证,以调用 API
以及调用 Chat API 和 Vertex AI API 等 Google Cloud 服务。
这意味着,初始化突发事件的用户必须拥有
服务。为了提高应用的稳健性,请考虑调用 Vertex AI API
而不是调用
/closeIncident 斜杠命令。有关配置服务账号的帮助
如需了解 Vertex AI API,请参阅
使用自定义服务账号。