IT Hands-on Lab

小規模組織向けIT環境の構築・運用に役立つ情報を、ハンズオン形式で紹介しています。

【Google Apps Script】Slack App × Gemini API使用 Slack Botアプリ(リアルタイム型)

【Google Apps Script】Slack App × Gemini API使用 Slack Botアプリ(リアルタイム型)

Google Apps ScriptのWebアプリにより、SlackのチャンネルやDMにメンション投稿された質問に対して、Slack AppとGemini APIを使用して、リアルタイムに自動回答するボットアプリの作成例を紹介します。

  • 操作環境:
    • OS:Windows 11
    • Webブラウザー:Google Chrome
  • 使用プラン:Google Workspace Business Starter、
     Slackフリープラン、Gemini API無料枠

 

 

アプリの仕様・構成

前の記事と同様、SlackのチャンネルやDMでSlackアプリをメンション(厳密にはSlackアプリとの1:1のDMはメンション不要)して質問が投稿されると、Google Apps Script(以降GASと表記します)により、Googleドライブの特定ファイルの情報を元に、Gemini APIで回答を作成し、自動回答する機能を実装します。
前の記事では、GASを15分おきに定期実行し、Slackに対象の投稿があれば回答する仕様としていました(定期巡回型)。
今回は、定期巡回型からアプリの構成を大きく変えず、リアルタイムに回答する仕様とします。処理のタイムアウト(GASは6分)により回答できずに取りこぼしが発生する可能性があります。
また、前の記事の最後に言及した、質疑内容のログ出力や未解決質問の管理者通知についても実装します。

図表1-1 アプリ構成図

図表1-1 アプリ構成図

 

Gemini API設定

前の記事の工程2と同じ設定のため、そちらをご覧ください。

 

Slack App設定

前の記事の工程3と同じ設定に加え、Events API使用のため、[Socket Mode]の無効化と[Event Subscriptions]の設定が必要となります。[Event Subscriptions]の図表3-8の設定は、工程6の図表6-6のGASのWebアプリのURL登録が必要なため、手順が前後します。

 

Slackにサインインした後、Slack APIのYour Apps画面にアクセスし、[Create an App]を選択します。
[Blank app]を選択し、[Coutinue]を選択します。

図表3-1 Slack App作成へ

図表3-1 Slack App作成へ

 

アプリ名(メンション時に指定するボットの名前)を入力し、使用ワークスペースを選択し、[Create]を選択します。
左メニューの[App Home]を選択します。
[Messages Tab]のトグルをオンにし、[Allow users to send Slach commands and messages from the messages tab]のチェックをオンにします。これにより、チャンネル内やDM内でこのSlackアプリへのメンションを使用できるようになります。

*トグルの設定を変更できない場合は、先に図表3-3の設定を行ってください

図表3-2 Slack App作成、Message Tab設定

図表3-2 Slack App作成、Message Tab設定

 

左メニューの[OAuth & Permissions]を選択します。
画面中央部にある[スコープ>ボットトークンのスコープ]の[OAuthスコープを追加する]より、自動応答を使用したいチャンネルやDMに応じて以下を選択します。

*今回は、パブリック/プライベートチャンネル、1:1/複数人のDMで使用する前提のため、設定が多くなっています。ご自身の環境で試す場合、使用しない会話の種類については設定不要です
  • chat:write
  • channels:history,channels:read(パブリックチャンネルで使用する場合)
  • groups:history,groups:read(プライベートチャンネルで使用する場合)
  • im:history,im:read(1:1DMで使用する場合)
  • mpim:history,mpim:read(複数人DMで使用する場合)
  • users:read(質疑内容のログ出力や未解決質問の管理者通知を行う場合)
*使用するSlack APIの仕様に応じてスコープを設定します。GASスクリプト内で使用する各Slack APIは工程7のポイント事項に公式ドキュメントのリンクを貼っていますので、参考にしてください

図表3-3 Slack App ボットトークンのスコープ設定

図表3-3 Slack App ボットトークンのスコープ設定

 

図表3-3の画面の上部にある[OAuth Tokens]の[install to (ワークスペース名)]を選択します。
[許可する]を選択します。
なお、この設定を行うユーザーがワークスペースの管理者でない場合は、管理者に許可依頼が通知され、管理者の許可を待つ形となります。

*図表3-3のスコープを変更した場合などにおいても、図表3-4の操作を求められることがあります

処理が完了すると、OAuth Tokenが発行されます。[Copy]を選択し、値を控えておきます。この値は工程6のGASのスクリプトプロパティで設定します。

図表3-4 Slack App トークン取得

図表3-4 Slack App トークン取得

 

左メニューの[Socket Mode]を選択し、 [Enable Socket Mode]のトグルをオフにします。

図表3-5 Slack App Socket Mode 無効化

図表3-5 Slack App Socket Mode 無効化

 

左メニューの[Event Subscriptions]を選択し、[Enable Events]のトグルをオンにし、[Subscribe to bot events]を選択して展開します。

図表3-6 Slack App Event Subscriptions Event有効化

図表3-6 Slack App Event Subscriptions Event有効化

 

[Add Bot User Event]より、自動応答を使用したいチャンネルやDMに応じて以下を選択します。

*今回は、パブリック/プライベートチャンネル、1:1/複数人のDMで使用する前提のため、設定が多くなっています。ご自身の環境で試す場合、使用しない会話の種類については設定不要です
  • message.channels(パブリックチャンネルで使用する場合)
  • message.groups(プライベートチャンネルで使用する場合)
  • message.im(1:1DMで使用する場合)
  • message.mpim(複数人DMで使用する場合)

図表3-7 Slack App Event Subscriptions bot events指定

図表3-7 Slack App Event Subscriptions bot events指定

 

画面上部の[Request URL]欄に工程6の図表6-6で控えたGASのWebアプリのURLをペーストします。
[Verified]と表示されれば問題ありません。右下の[Save Changes]を選択します。

図表3-8 Slack App Event Subscriptions Request URL設定

図表3-8 Slack App Event Subscriptions Request URL設定

 

Slack設定

前の記事の工程4と同じ設定(*)に加え、管理者通知用チャンネルを用意し、工程3のSlackアプリを招待しておきます。

*手順は前の記事をご参照ください。その際、Slackアプリは今回のものに読み替えてください

 

管理者通知用チャンネルにメンバー追加と同じ要領で、「/invite @{Slackアプリ名}」で招待できます。
併せて、URLの末尾のチャンネルID(Cxxxxxxxxxx)を控えておきます。この値は工程6のGASのスクリプトプロパティ内で使用します。

図表4-1 管理者通知用チャンネルへのSlackアプリの招待

図表4-1 管理者通知用チャンネルへのSlackアプリの招待

 

Googleドライブ 情報ファイル用意

前の記事の工程5と同じ設定(手順はそちらをご参照)に加え、Googleドライブにログ出力用のスプレッドシートを配置します。

この例では、共有ドライブの[IT管理]の下に[user_manual_and_FAQ]フォルダーの下に、ログ出力用のスプレッドシートを配置します。

図表5-1 Googleドライブへのログ出力用スプレッドシート配置

図表5-1 Googleドライブへのログ出力用スプレッドシート配置

 

図表5-2はログ出力用スプレッドシートの内容です。1行目に項目名を入力しておきます。2行目以降はサンプルなのでブランクで問題ありません。
併せて、URL欄の「・・・/d/xxxxxx/edit?・・・」のxxxxxx部分の値(ファイルID)を控えておきます。工程6のGASのスクリプトプロパティで使用します。

図表5-2 ログ出力用スプレッドシート(参考)

図表5-2 ログ出力用スプレッドシート(参考)

 

Google Apps Script用意

WebブラウザーにてGASのホーム画面にアクセスし、[新しいプロジェクト]を選択します。
[無題のプロジェクト]を選択し、プロジェクト名を分かり易い名称に変更します。
左端の[プロジェクトの設定]アイコンを選択します。

図表6-1 GASプロジェクト作成・名称変更、スクリプトプロパティへ

図表6-1 GASプロジェクト作成・名称変更、スクリプトプロパティへ

 

画面下部の[スクリプトプロパティを追加]を選択します。
以下の通り設定し、[スクリプトプロパティを保存]を選択します。

  • admin_channel_id:工程4の図表4-1で控えたチャンネルIDの値
  • faq_spreadsheet_id:前の記事の工程5の図表5-3で控えたファイルIDの値
  • gemini_api_key:前の記事の工程2の図表2-2で控えたAPIキーの値
  • log_spreadsheet_id:工程5の図表5-2で控えたファイルIDの値
  • pdf_folder_id:前の記事の工程5の図表5-1で控えたフォルダーIDの値
  • slack_bot_token:工程3の図表3-4で控えたOAuth Tokenの値

図表6-2 GASスクリプトプロパティ追加

図表6-2 GASスクリプトプロパティ追加

 

スクリプトエディターのコード.gsにスクリプトを記述していきます(スクリプトの内容は工程7で紹介します)。

図表6-3 GASスクリプトエディター

図表6-3 GASスクリプトエディター

 

Webアプリとして初回デプロイを行います。

 

右上の[デプロイ>新しいデプロイ]を選択します。
設定アイコンを選択し、[ウェブアプリ]を選択します。
[新しいデプロイ]画面で以下の通り設定し、[デプロイ]を選択します。

  • 次のユーザーとして実行:自分
  • アクセスできるユーザー:全員
*[アクセスできるユーザー]の選択肢に「全員」が表示されない場合は、図表6-8の設定を行ってください

図表6-4 初回デプロイ(実行)

図表6-4 初回デプロイ(実行)

 

データアクセスに関する確認メッセージが表示されたら、[アクセスを承認]を選択します。
アプリ承認に関する確認メッセージが表示されたら、[Select all]にチェックをつけ、[Continiue]を選択します。

図表6-5 初回デプロイ(アクセス承認)

図表6-5 初回デプロイ(アクセス承認)

 

デプロイが完了すると、WebアプリのURLが表示されます。
このURLの[コピー]を選択し、工程3の図表3-8のURL欄に設定します。

図表6-6 初回デプロイ(完了)

図表6-6 初回デプロイ(完了)

 

スクリプトを変更した場合、アプリにそれを反映させるため、再度デプロイを行う必要があります。

 

右上の[デプロイ>デプロイを管理]を選択します。
右上の編集アイコンを選択します。
バージョン欄で[新バージョン]を選択し、[デプロイ]を選択します。

図表6-7 二回目以降のデプロイ(参考)

図表6-7 二回目以降のデプロイ(参考)

 

デプロイ時にアクセスユーザーで「公開」の選択肢が表示されない場合、Google管理コンソールで設定を変更する必要があります。
Google管理コンソールの左メニューの[アプリ>Google Workspace>ドライブとドキュメント]を選択し、適用対象の組織部門を選択し(*)、[共有設定>共有オプション]の[{組織名}の外部との共有]で[オン]を選択します。

*組織全体のこの設定が[オフ]または[許可リスト登録済ドメイン、・・・]が選択状態の場合、[オン]に変更するとセキュリティポリシーを緩めることになります。全組織に対し[オン]にするより、今回のGASを実行するユーザーの組織部門に限定する方が安全です。組織のセキュリティポリシー上、外部共有を許容できない場合は、前の記事のような別の実装方式をご検討ください

図表6-8 Google管理コンソール ドライブとドキュメント 外部共有の設定変更

図表6-8 Google管理コンソール ドライブとドキュメント 外部共有の設定変更

 

Google Apps Scriptによる処理実装

スクリプトが長いため見出しをメイン関数とサブ関数に分けますが、どちらもコード.gs内に記述します。

 

doPost, メイン関数

ポイント事項は以下の通りです。

  • doPost:SlackのEvents APIではリクエストに対し3秒以内に応答がないと3回再送する(公式ドキュメント)ため、重複イベントかどうかをチェックし、重複処理を回避している。
  • handleSlackEvent(メイン処理):以下を除き、基本的な処理の流れは前の記事の定期巡回型とほぼ同じである。
    • 再実行の管理がないため、スクリプトプロパティの前回処理時タイムスタンプに関する処理がない
    • リクエスト情報(対象の質問)は、doPost関数を経て、引数のeventから取得している(65-94行目)
    • 質疑内容のログ出力と、未解決質問の管理者通知を実装した(113-142行目)
/**
 * GoogleDriveのFAQやマニュアルPDFを元に、Slackに投稿された質問に自動回答するBot
 */
/**
 * Slackから通知を受取り、同一処理内でAI回答の作成・返信を実行する 
 */
function doPost(e) {
  const json = JSON.parse(e.postData.contents);

  // URL検証(Challenge)への即時応答
  if (json.type === 'url_verification') {
    return ContentService.createTextOutput(json.challenge);
  }

  // Slackのリトライ(再送)ヘッダーをチェックして重複実行を拒否
  // (Slack APIは3秒以内に応答がないと3回再送する仕様のため)
  if (e.parameter && e.parameter['X-Slack-Retry-Num']) {
    console.log(`リトライリクエストのため無視します (Retry: ${e.parameter['X-Slack-Retry-Num']})`);
    return ContentService.createTextOutput("OK");
  }

  if (json.event) {
    const event = json.event;

    // 重複イベントをチェックする(CacheServiceを利用)
    const cache = CacheService.getScriptCache();
    const eventId = json.event_id || (event.user + "_" + event.ts);
    if (cache.get(eventId)) {
      console.log(`重複イベントのため処理をスキップします: ${eventId}`);
      return ContentService.createTextOutput("OK");
    }
    // 5分間キャッシュに保持して重複処理を防止する
    cache.put(eventId, "processed", 300);

    // Bot自身の投稿は即時スキップする
    if (event.bot_id || event.subtype) {
      return ContentService.createTextOutput("OK");
    }

    // メイン処理を実行
    handleSlackEvent(event); //★メイン関数呼出
  }

  return ContentService.createTextOutput("OK");
}

/**
 * メイン関数
 */
function handleSlackEvent(event) {
  const botToken = PropertiesService.getScriptProperties().getProperty('slack_bot_token');
  const FAQ_SPREADSHEET_ID = PropertiesService.getScriptProperties().getProperty('faq_spreadsheet_id');
  const PDF_FOLDER_ID = PropertiesService.getScriptProperties().getProperty('pdf_folder_id');
  const LOG_SPREADSHEET_ID = PropertiesService.getScriptProperties().getProperty('log_spreadsheet_id');
  const ADMIN_CHANNEL_ID = PropertiesService.getScriptProperties().getProperty('admin_channel_id');

  // Bot自身のユーザーID(Uxxxxxxx)を取得する
  const botUserId = getBotUserId(botToken);//★サブ関数呼出
  if (!botUserId) {
    console.error("BotのユーザーIDが取得できませんでした。");
    return;
  }

  // 投稿された会話の種類をチェックする
  const channelId = event.channel;
  const channelType = event.channel_type;
  const isDm = channelType === 'im' || channelType === 'mpim';

  // 投稿された会話の種類がチャンネルの場合:許可リストチェック
  if (!isDm) {
    //対象チャンネルのIDを指定する(パブリック/プライベートは問わない)
    const ALLOWED_CHANNEL_IDS = ['Cxxxxxxxxxx', 'Cxxxxxxxxxx'];
    if (!ALLOWED_CHANNEL_IDS.includes(channelId)) return;
  }

  // メンションのないチャンネル投稿は対象外とする
  const mentionTag = `<@${botUserId}>`;
  const isMentioned = isDm || (event.text && event.text.includes(mentionTag));
  if (!isMentioned) return;

  // メンションタグを除去して質問文を抽出する
  let userQuery = event.text ? event.text.replace(new RegExp(mentionTag, 'g'), '').trim() : '';
  if (!userQuery) return;

  // スレッドの親IDを取得する(親メッセージがない場合は自身のts)
  const threadTs = event.thread_ts || event.ts;
  let historyText = "";

  // 親メッセージと異なる場合(=スレッド内の子返信の場合)に会話履歴を作成する
  if (event.thread_ts) {
    const threadMessages = getThreadMessages(channelId, event.thread_ts, botToken);//★サブ関数呼出

    // 最新の質問以外の過去ログ(ユーザーの発言とBotの回答)を抽出してテキスト化する
    const pastMsgs = threadMessages.filter(m => Number(m.ts) < Number(event.ts));
    historyText = pastMsgs.map(m => {
      const sender = m.bot_id ? "Bot" : "ユーザー";
      const cleanText = m.text ? m.text.replace(new RegExp(mentionTag, 'g'), '').trim() : '';
      return `${sender}: ${cleanText}`;
    }).join("\n");
  }

  try {
    // Googleドライブの情報読込(FAQスプレッドシート、ユーザーマニュアルPDF)
    const cachedFaqText = getSpreadsheetText(FAQ_SPREADSHEET_ID);//★サブ関数呼出
    const cachedPdfBlobs = getPdfBlobsFromFolder(PDF_FOLDER_ID);//★サブ関数呼出

    // Gemini API にテキスト(FAQ)と複数PDF、質問を渡して回答を得る
    const answer = askGemini(userQuery, historyText, cachedFaqText, cachedPdfBlobs);//★サブ関数呼出

    //Slackのスレッドに回答を投稿する
    postToSlackThread(channelId, threadTs, answer, botToken);//★サブ関数呼出

    // ユーザー名・チャンネル名の取得(ログ記録・管理者通知用)
    const isUnresolved = answer.includes("別途担当者より回答いたします");
    const userName = getSlackUserName(event.user, botToken);//★サブ関数呼出
    const locationName = isDm ? "1:1 DM" : getSlackChannelName(channelId, botToken);//★サブ関数呼出

    // ログスプレッドシートへ全件ログを書き込み
    if (LOG_SPREADSHEET_ID) {
      try {
        const sheet = SpreadsheetApp.openById(LOG_SPREADSHEET_ID).getActiveSheet();
        sheet.appendRow([
          new Date(),                     // A列: 投稿日時
          locationName,                   // B列: チャンネル名または DM
          userName,                       // C列: 質問者の表示名
          userQuery,                      // D列: 質問内容
          answer,                         // E列: Botの回答
          isUnresolved ? "要対応" : "完了", // F列: ステータス
          channelId,                      // G列: チャンネルID
          event.user                      // H列: ユーザーID
        ]);
      } catch (logErr) {}
    }

    // 未解決の場合のみ管理者チャンネルへ通知
    if (isUnresolved && ADMIN_CHANNEL_ID) {
      const noticeText = `⚠️ *未解決の問い合わせが発生しました*\n` +
        `・*場所*: ${locationName}\n` +
        `・*質問者*: ${userName} (<@${event.user}>)\n` +
        `・*質問内容*:\n> ${userQuery.replace(/\n/g, '\n> ')}`;
      postToSlackThread(ADMIN_CHANNEL_ID, "", noticeText, botToken);
    }

  } catch (e) {
    console.error(`全体エラー: ${e.toString()}`);
  }
}

 

サブ関数

getBotUserId, getThreadMessages, getSpreadsheetText, getPdfBlobsFromFolder, askGemini, postToSlackThreadについては前の記事と同じため、そちらの説明をご覧ください。
その他のサブ関数のポイント事項は以下の通りです。

  • getSlackUserName:Slack APIでユーザーIDからユーザー名の取得を行っている
  • getSlackChannelName: Slack APIでチャンネルIDからチャンネル名の取得を行っている
/**
 * Bot自身のユーザーID(Uxxxxxxx)を取得する関数
 */
function getBotUserId(token) {
  const url = 'https://slack.com/api/auth.test';
  const response = UrlFetchApp.fetch(url, {
    method: 'post',
    headers: { 'Authorization': `Bearer ${token}` }
  });
  const data = JSON.parse(response.getContentText());
  return data.ok ? data.user_id : null;
}

/**
 * スレッド内の全メッセージ履歴を取得する関数
 */
function getThreadMessages(channelId, threadTs, token) {
  const url = `https://slack.com/api/conversations.replies?channel=${channelId}&ts=${threadTs}`;
  try {
    const res = UrlFetchApp.fetch(url, {
      method: 'get',
      headers: { 'Authorization': `Bearer ${token}` }
    });
    const data = JSON.parse(res.getContentText());
    return data.ok ? data.messages : [];
  } catch (e) {
    console.error("スレッド履歴取得エラー:", e.toString());
    return [];
  }
}

/**
 * スプレッドシートの内容を取得して文字列テキストに変換する関数
 */
function getSpreadsheetText(spreadsheetId) {
  const ss = SpreadsheetApp.openById(spreadsheetId);
  const sheet = ss.getSheets()[0];
  const values = sheet.getDataRange().getValues();
  return values.map(row => row.join("\t")).join("\n");
}

/**
 * 指定したGoogle Driveフォルダー内の全PDFファイルのBlobを取得する関数
 */
function getPdfBlobsFromFolder(folderId) {
  const folder = DriveApp.getFolderById(folderId);
  const files = folder.getFilesByType(MimeType.PDF);
  const pdfBlobs = [];

  while (files.hasNext()) {
    pdfBlobs.push(files.next().getBlob());
  }

  if (pdfBlobs.length === 0) {
    console.warn("警告: 指定されたフォルダーにPDFファイルが見つかりませんでした。");
  }

  return pdfBlobs;
}

/**
 * Gemini API にテキスト(FAQ)と複数PDF、質問を渡して回答を得る関数
 */
function askGemini(query, historyText, faqText, pdfBlobs) {
  const apiKey = PropertiesService.getScriptProperties().getProperty('gemini_api_key');
  if (!apiKey) {
    throw new Error("スクリプトプロパティ 'gemini_api_key' が設定されていません。");
  }

  const modelName = "gemini-3.6-flash";
  const url = `https://generativelanguage.googleapis.com/v1beta/models/${modelName}:generateContent?key=${apiKey}`;

  const systemInstruction = 
    "あなたは社内ヘルプデスクのFAQ回答Botです。\n" +
    "提供された【FAQデータ】および添付された【複数のマニュアルPDF】の情報のみに基づいて、ユーザーからの質問にわかりやすく丁寧に変更・回答してください。\n" +
    "スレッド内での追加質問の場合は【これまでの会話履歴】を踏まえて回答してください。\n" +
    "資料に記載がない内容については、無理に答えず「別途担当者より回答いたします。」と回答してください。";

  // 会話履歴がある場合はプロンプトに含める
  let userPrompt = `【FAQデータ】\n${faqText}\n\n`;
  if (historyText) {
    userPrompt += `【これまでの会話履歴】\n${historyText}\n\n`;
  }
  userPrompt += `【最新の質問】\n${query}`;

  const parts = [];

  // PDFファイルをパーツに追加
  pdfBlobs.forEach(pdfBlob => {
    parts.push({
      "inline_data": {
        "mime_type": "application/pdf",
        "data": Utilities.base64Encode(pdfBlob.getBytes())
      }
    });
  });

  parts.push({
    "text": `${systemInstruction}\n\n${userPrompt}`
  });

  const options = {
    "method": "post",
    "contentType": "application/json",
    "payload": JSON.stringify({
      "contents": [{ "parts": parts }],
      "generationConfig": { "temperature": 0.2 }
    }),
    "muteHttpExceptions": true
  };

  const response = UrlFetchApp.fetch(url, options);
  const statusCode = response.getResponseCode();
  const json = JSON.parse(response.getContentText());

  if (statusCode === 200) {
    return json.candidates[0].content.parts[0].text;
  }

  throw new Error(`Gemini API エラー (${statusCode}): ${JSON.stringify(json)}`);
}

/**
 * Slackのスレッドに回答を投稿する関数
 */
function postToSlackThread(channelId, threadTs, text, token) {
  const url = 'https://slack.com/api/chat.postMessage';
  const payload = {
    channel: channelId,
    thread_ts: threadTs,
    text: text
  };

  UrlFetchApp.fetch(url, {
    method: 'post',
    contentType: 'application/json; charset=utf-8',
    headers: { 'Authorization': `Bearer ${token}` },
    payload: JSON.stringify(payload)
  });
}

/**
 * ユーザーID(Uxxxx)から表示名(DisplayNameまたはRealName)を取得する関数
 */
function getSlackUserName(userId, token) {
  if (!userId) return "不明";
  const url = `https://slack.com/api/users.info?user=${userId}`;
  try {
    const res = UrlFetchApp.fetch(url, {
      method: 'get',
      headers: { 'Authorization': `Bearer ${token}` }
    });
    const data = JSON.parse(res.getContentText());
    if (data.ok && data.user) {
      return data.user.profile.display_name || data.user.profile.real_name || data.user.name;
    }
  } catch (e) {
    console.error("ユーザー名取得エラー:", e.toString());
  }
  return userId;
}

/**
 * チャンネルID(Cxxxx)からチャンネル名を取得する関数
 */
function getSlackChannelName(channelId, token) {
  if (!channelId) return "不明";
  const url = `https://slack.com/api/conversations.info?channel=${channelId}`;
  try {
    const res = UrlFetchApp.fetch(url, {
      method: 'get',
      headers: { 'Authorization': `Bearer ${token}` }
    });
    const data = JSON.parse(res.getContentText());
    if (data.ok && data.channel) {
      return `#${data.channel.name}`;
    }
  } catch (e) {
    console.error("チャンネル名取得エラー:", e.toString());
  }
  return channelId;
}

 

動作確認

ユーザーがSlackアプリをメンションして(厳密にはSlackアプリとの1:1のDMはメンション不要)質問すると、15分おきに定期実行しているGASにより、返信にて自動回答されます。
図表8-1は1:1のDMに投稿した質問の自動回答例です。
この例では工程5で準備したGoogleドライブ内のファイルに情報があったため、詳しい操作手順が回答されています。

図表8-1 動作確認 DM 情報があった場合

図表8-1 動作確認 DM 情報があった場合

 

図表8-2はSlackアプリが招待されたチャンネルに投稿した質問の自動回答例です。
この例では工程5で準備したGoogleドライブ内のファイルに情報がなかったため、担当者から別途回答することとしています。

図表8-2 動作確認 チャンネル 情報がなかった場合(返信)

図表8-2 動作確認 チャンネル 情報がなかった場合(返信)

 

図表8-2のようにGoogleドライブ内のファイルに情報がなく担当者から別途回答とされた場合、図表8-3のように管理者通知用チャンネルに通知されます。

図表8-3 動作確認 チャンネル 情報がなかった場合(管理者通知)

図表8-3 動作確認 チャンネル 情報がなかった場合(管理者通知)

 

ここでは割愛しますが、スプレッドシートへの質疑内容のログ出力は工程5の図表5-2をご参照ください。

 

おわりに(方式比較)

前の記事で紹介した定期実行型との違いを下表にまとめます。

比較項目/方式 定期巡回型 リアルタイム型
回答のタイムラグ ×(あり) ○(なし)
失敗時再実行 ○(あり) ×(なし)
GWS外部共有許可 ○(不要) ×(必要:工程6の図表6-8)
GASメンテナンス性 ×(*表の下の注記参照) ○
図表9-1 方式比較(定期巡回型,リアルタイム型)
*前の記事の工程8の図表8-4の通り、回答取りこぼし時の再実行を制御するため、スクリプトプロパティに質問の発生したチャンネルのタイムスタンプ値を保存します。対象チャンネルが増えると管理が煩雑になります(退職者との1:1DMを削除するなど)

 

どちらの方式でも一度のGASの処理で6分超かかるとタイムアウトするため、回答の材料となる情報が大量にある場合や質問が多発して負荷が集中する場合には向いていません。GASの6分タイムアウトに抵触する場合は、GASではなくGoogle Cloudを使用する等、別のアプリ構成での実装が必要になります。

 

 

当ブログ内の関連記事

elmgrn.hatenablog.com