LinebotとGASの設定

【初めてでも10分で作れる】GASで作るお問い合わせLINEbot【GAS×LINE Messaging API】

Gemini APIとGASを活用したLINEBOT開発レシピ
を参考に
スマホから送信するLINEbot を作成できるようにする

現状だとスマホからの送信のみ対応できていない

これを使うには
Google Apps Scriptの料金を調べる
調べた結果
料金は無料だけど制約がある

とりあえず LINE bot の設定をする
https://developers.line.biz/ja/docs/messaging-api/getting-started/
へアクセスし
コンソールをクリックし
LINEアカウントでログイン

ログインはQRコードからできるので
LINEアプリを起動しQRを読み込み
そこに表示された数字を入力すればログインできる

ログインしたら新規プロパイダーを作成

これは他と被らないものにすればOK

チャネル設定をmessaging API にして
作成しようとしたら
公式アカウントを作成しないとだめになっている

とりあえず以前作成したことはあるので
新しい公式アカウントを作成する

次にチャネル作成

これは設定が変わっていて
公式アカウントリストの中から
今回作成した公式あかうんとをクリックし
設定をクリックすると
Messaging API の項目が出てくる

これで
Messaging API を利用する
をクリックし
プロバイダーを選択する

WebhookのURLは後で設定できるのでスキップしてもOK

Channel ID

Channel secret
をコピーしておく

次に
応答設定をクリックし
応答メッセージをOFFにする

次に
LINE Developers
https://developers.line.biz/ja/
へアクセスし
コンソールをクリック

プロバイダー一覧から
先ほど設定したプロバイダーを選択

チャネル選択しクリック

Messaging API設定
をクリック

チャネルアクセストークン
で発行をクリックする

次に
チャットボットの裏側となるGoogleAppsScriptの設定を実施

Google アカウントでログインした状態でアクセスし
コピーを作成する

コピーができたら、【設定】シートの【セルC2】に、先ほどメモしましたLINEのチャネルアクセストークンを貼り付け

次に
【拡張機能】⇒【Apps Script】を選択すると、AppsScriptの画面が開く

【デプロイ】⇒【新しいデプロイ】を選択し
* 次のユーザーとして実行 自分(〇〇〇〇〇@gmail.com)
* アクセスできるユーザー 全員
を選択し、【デプロイ】を選択

新しいデプロイを初回実行すると、【アクセスの承認】を押下して、その後利用するアカウントを選択します。
すると、【このアプリはGoogleで確認されていません】と注意書きがでますので
左下の詳細を選択します。(ここで【安全なページに戻る】を押下すると元に戻ってしまうので、デプロイからやり直してください。)
詳細が表示されたら、【ファイル名+(安全でないページ)に移動】が表示されますので、そちらを選択します。
Googleアカウントへのアクセスをリクエストしてくるので【許可】を選択

デプロイ ID
ウェブアプリ
URL
が表示されているので
このURLを LINEbot の web hook に設定する

そして
LINE Developers
https://developers.line.biz/ja/

Messaging API設定

Webhookの利用
をONにする

これで
スマホからlinebot で
こんにちわ
と入力すると
自動でメッセージが返ってくる

とりあえず実験は成功

続いて

Gemini APIとGASを活用したLINEBOT開発レシピ
を実践する

* 1分間あたり15リクエスト
* 1分あたり100万トークン
•1日あたり1500回のリクエスト
がgemini の無料枠

コードをそのまま使うので
前回と同じようにコピーする

Channel ID

Channel secret

チャネルアクセストークン
を忘れないようにメモしておく

GASでlinebot

GASでlinebot

Line notify が廃止されるので代替を linebot にする必要がある

var SS = SpreadsheetApp.getActiveSpreadsheet(); //SpreadsheetのURL
var sheet = SS.getSheetByName("問い合わせ内容"); //Spreadsheetのシート名(タブ名)


var Trainingsheet= SS.getSheetByName('応答メッセージ');
var Settingsheet= SS.getSheetByName('設定');
var DBsheet= SS.getSheetByName('チャットボット用DB');
var keywordsheet= SS.getSheetByName('キーワード登録済み単語');


const CHANNEL_ACCESS_TOKEN = Settingsheet.getRange("C2").getValue();

//DBシート
var kensaku_words = DBsheet.getRange("A:A").getValues();
var kaitou_array = DBsheet.getRange("B:B").getValues();
var keyword_array = keywordsheet.getRange("A:A").getValues();

var kensaku_words1 = Array.prototype.concat.apply([], kensaku_words);
var kaitou_array1 = Array.prototype.concat.apply([], kaitou_array);
var keyword_array1 = Array.prototype.concat.apply([], keyword_array);




function doPost(request) {



 //POSTリクエストをJSONデータにパース
 const receiveJSON = JSON.parse(request.postData.contents);
 const event = receiveJSON.events[0];

var replytext =""


//検索ワードDBの何個目に送信されたメッセージが該当するか検索(ない場合は【-1】が帰ってきます。)
  j = kensaku_words1.indexOf(event.message.text) 


  if (j != -1.0){ // messageに、DBの文字列が含まれるか検索、含まれていたら
    replytext =  kaitou_array1[j];
    replyToUser(event.replyToken, replytext)  

    }else if(keyword_array1.indexOf(event.message.text)!= -1.0 ){
    replytext = "登録済応答メッセージ回答"
    }else{
    replytext =  keywordsheet.getRange(2,5).getValue();
    replyToUser(event.replyToken, replytext)  
    }

//送られてきたデータをlogシートに記載
sheet.appendRow([getCurrentTime(),event.message.text,replytext]);


}

function getCurrentTime() {
  //日付の宣言
  return Utilities.formatDate(new Date(), "Asia/Tokyo", "yyyy/MM/dd HH:mm:ss");
}

の問題点として
問題点と改善点
1. CHANNEL_ACCESS_TOKEN を直接 Settingsheet から取得している
* 問題点:
* CHANNEL_ACCESS_TOKEN をスプレッドシートから取得するのは安全ではない(シートが漏洩した場合、外部から悪用される可能性)。
* 改善策:
* Google Apps Script のスクリプトプロパティ(PropertiesService)を使用する。
* Settingsheet.getRange(“C2”).getValue(); を PropertiesService.getScriptProperties().getProperty(“CHANNEL_ACCESS_TOKEN”) に変更。
2. kensaku_words1, kaitou_array1, keyword_array1 の取得方法
* 問題点:
* スプレッドシートのデータ全体を毎回読み込むのは非効率(特に行数が増えた場合、実行速度が遅くなる)。
* 改善策:
* 1回の getDataRange().getValues() で配列を取得し、flat() でフラット化する。
3. doPost のエラーハンドリングがない
* 問題点:
* 予期しないデータ(不正なJSON、nullなど)が来たときの処理がない。
* 改善策:
* try-catch を導入し、エラーが発生した場合のログ出力と適切なエラーレスポンスを返す。
4. event.message.text が undefined の場合の処理がない
* 問題点:
* event.message.text が undefined の場合、.indexOf() の実行時にエラーが発生する可能性がある。
* 改善策:
* event.message && event.message.text を確認して処理する。
5. replyToUser() の呼び出しが一部抜けている
* 問題点:
* keyword_array1.indexOf(event.message.text)!= -1.0 の場合、replyToUser() を呼んでいない。
* 改善策:
* replyToUser() を適切に呼び出す。

となっている

これを

var SS = SpreadsheetApp.getActiveSpreadsheet();
var sheet = SS.getSheetByName("問い合わせ内容");
var Trainingsheet = SS.getSheetByName('応答メッセージ');
var Settingsheet = SS.getSheetByName('設定');
var DBsheet = SS.getSheetByName('チャットボット用DB');
var keywordsheet = SS.getSheetByName('キーワード登録済み単語');

// セキュアな方法でアクセストークンを取得
const CHANNEL_ACCESS_TOKEN = PropertiesService.getScriptProperties().getProperty("CHANNEL_ACCESS_TOKEN");

// DBデータを一括取得
var kensaku_words1 = DBsheet.getDataRange().getValues().map(row => row[0]).flat();
var kaitou_array1 = DBsheet.getDataRange().getValues().map(row => row[1]).flat();
var keyword_array1 = keywordsheet.getDataRange().getValues().map(row => row[0]).flat();

function doPost(request) {
  try {
    // POSTデータをパース
    const receiveJSON = JSON.parse(request.postData.contents);

    // イベントがない、または不正な場合は処理しない
    if (!receiveJSON.events || receiveJSON.events.length === 0) {
      return ContentService.createTextOutput("No event").setMimeType(ContentService.MimeType.TEXT);
    }

    const event = receiveJSON.events[0];

    // メッセージがない、またはテキストがない場合は処理しない
    if (!event.message || !event.message.text) {
      return ContentService.createTextOutput("No message text").setMimeType(ContentService.MimeType.TEXT);
    }

    var replytext = "";

    // 検索ワードDBにメッセージがあるかチェック
    var j = kensaku_words1.indexOf(event.message.text);

    if (j !== -1) {
      // 返信メッセージを取得
      replytext = kaitou_array1[j];
    } else if (keyword_array1.includes(event.message.text)) {
      // 登録済みの応答メッセージ
      replytext = "登録済応答メッセージ回答";
    } else {
      // デフォルトメッセージを取得
      replytext = keywordsheet.getRange(2, 5).getValue();
    }

    // LINEに返信
    replyToUser(event.replyToken, replytext);

    // 送信データをスプレッドシートに記録
    sheet.appendRow([getCurrentTime(), event.message.text, replytext]);

    return ContentService.createTextOutput("Success").setMimeType(ContentService.MimeType.TEXT);

  } catch (error) {
    Logger.log("Error in doPost: " + error.toString());
    return ContentService.createTextOutput("Error processing request").setMimeType(ContentService.MimeType.TEXT);
  }
}

// 現在の時間を取得
function getCurrentTime() {
  return Utilities.formatDate(new Date(), "Asia/Tokyo", "yyyy/MM/dd HH:mm:ss");
}

// LINEに返信
function replyToUser(replyToken, message) {
  var url = "https://api.line.me/v2/bot/message/reply";
  var payload = {
    "replyToken": replyToken,
    "messages": [{ "type": "text", "text": message }]
  };

  var options = {
    "method": "post",
    "headers": {
      "Content-Type": "application/json",
      "Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN
    },
    "payload": JSON.stringify(payload)
  };

  try {
    UrlFetchApp.fetch(url, options);
  } catch (error) {
    Logger.log("Error in replyToUser: " + error.toString());
  }
}

として保存

改善点まとめ
✅ アクセストークンを PropertiesService に保存し、スプレッドシートの流出リスクを防止。
✅ スプレッドシートのデータ取得を最適化し、getDataRange().getValues() で一括取得。

✅ エラーハンドリング (try-catch) を追加し、不正なリクエストの処理を安全に。

✅ イベントデータの存在確認 (event.message && event.message.text) を追加し、エラー防止。

✅ replyToUser() のエラーハンドリングを強化し、ログ出力を追加。

✅ keyword_array1.indexOf() の処理を includes() に変更し、可読性を向上。

✅ 関数を整理し、再利用しやすく。

Google Apps Script の スクリプトプロパティ (ScriptProperties) を使うと、スプレッドシートに機密情報を保存せずに管理できます。CHANNEL_ACCESS_TOKEN のような LINEのアクセストークン を安全に保存するのに適しています

スクリプトエディタで スクリプトプロパティ を設定するには、以下の手順を実行します。
① スクリプトプロパティにアクセストークンを保存
1. Google Apps Script エディタ を開く
* スプレッドシートの 拡張機能 → Apps Script を開く
2. エディタの左側メニュー で ⚙️プロジェクトの設定 を開く
3. 下の方にある スクリプトプロパティ の + プロパティを追加 をクリック
4. プロパティ名: CHANNEL_ACCESS_TOKEN
5. 値: <あなたのLINEアクセストークン> を入力して 保存 をクリック

ということなので
プロジェクトの設定をクリック
スクリプトプロパティの追加

プロパティ名: CHANNEL_ACCESS_TOKEN
値: <あなたのLINEアクセストークン> を入力して 保存

アクセストークンの値を一度削除して

var SS = SpreadsheetApp.getActiveSpreadsheet();
var sheet = SS.getSheetByName("問い合わせ内容");
var Trainingsheet = SS.getSheetByName('応答メッセージ');
var Settingsheet = SS.getSheetByName('設定');
var DBsheet = SS.getSheetByName('チャットボット用DB');
var keywordsheet = SS.getSheetByName('キーワード登録済み単語');

// セキュアな方法でアクセストークンを取得
const CHANNEL_ACCESS_TOKEN = PropertiesService.getScriptProperties().getProperty("CHANNEL_ACCESS_TOKEN");

// DBデータを一括取得
var kensaku_words1 = DBsheet.getDataRange().getValues().map(row => row[0]).flat();
var kaitou_array1 = DBsheet.getDataRange().getValues().map(row => row[1]).flat();
var keyword_array1 = keywordsheet.getDataRange().getValues().map(row => row[0]).flat();

function doPost(request) {
  try {
    // POSTデータをパース
    const receiveJSON = JSON.parse(request.postData.contents);

    // イベントがない、または不正な場合は処理しない
    if (!receiveJSON.events || receiveJSON.events.length === 0) {
      return ContentService.createTextOutput("No event").setMimeType(ContentService.MimeType.TEXT);
    }

    const event = receiveJSON.events[0];

    // メッセージがない、またはテキストがない場合は処理しない
    if (!event.message || !event.message.text) {
      return ContentService.createTextOutput("No message text").setMimeType(ContentService.MimeType.TEXT);
    }

    var replytext = "";

    // 検索ワードDBにメッセージがあるかチェック
    var j = kensaku_words1.indexOf(event.message.text);

    if (j !== -1) {
      // 返信メッセージを取得
      replytext = kaitou_array1[j];
    } else if (keyword_array1.includes(event.message.text)) {
      // 登録済みの応答メッセージ
      replytext = "登録済応答メッセージ回答";
    } else {
      // デフォルトメッセージを取得
      replytext = keywordsheet.getRange(2, 5).getValue();
    }

    // LINEに返信
    replyToUser(event.replyToken, replytext);

    // 送信データをスプレッドシートに記録
    sheet.appendRow([getCurrentTime(), event.message.text, replytext]);

    return ContentService.createTextOutput("Success").setMimeType(ContentService.MimeType.TEXT);

  } catch (error) {
    Logger.log("Error in doPost: " + error.toString());
    return ContentService.createTextOutput("Error processing request").setMimeType(ContentService.MimeType.TEXT);
  }
}

// 現在の時間を取得
function getCurrentTime() {
  return Utilities.formatDate(new Date(), "Asia/Tokyo", "yyyy/MM/dd HH:mm:ss");
}

// LINEに返信
function replyToUser(replyToken, message) {
  var url = "https://api.line.me/v2/bot/message/reply";
  var payload = {
    "replyToken": replyToken,
    "messages": [{ "type": "text", "text": message }]
  };

  var options = {
    "method": "post",
    "headers": {
      "Content-Type": "application/json",
      "Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN
    },
    "payload": JSON.stringify(payload)
  };

  try {
    UrlFetchApp.fetch(url, options);
  } catch (error) {
    Logger.log("Error in replyToUser: " + error.toString());
  }
}

とコード変更

比べたらやはりURLが変わっている

この変更後のURLをlinebot の web hook に再度設定すれば動作する

次はDB関連の変更

function training() {
  
  var lastrow = Trainingsheet.getLastRow();
  var lastcolumn = Trainingsheet.getLastColumn();
  
  //Trainingシートのタイトル行除く全てのデータを取得
  // var Alldata  = Trainingsheet.getRange(2,2,lastrow-1,lastcolumn-1).getValues();
  var Alldata  = Trainingsheet.getRange(2,1,lastrow,lastcolumn).getValues();
  
  var DB_lastrow = DBsheet.getLastRow();
  var DB_lastcolumn = DBsheet.getLastColumn();
  
  //DBシートのデータ消去
  if(DB_lastrow!=2){
    DBsheet.getRange(3,1,DB_lastrow-2,DB_lastcolumn).clear();
  }
  
  var count = 0;
  
  for(var i = 0; i < Alldata.length; i++) {
    
    for(var j = 0; j < Alldata[i].length-1; j++){
      
      if(Alldata[i][j+1] != ""){
        DBsheet.getRange(3+count,1).setValue(Alldata[i][j+1]);//応答メッセージ
        
        DBsheet.getRange(3+count,2).setValue(Alldata[i][0]);//キーワード
        count++
          
      }
    }
  }
  
}

のコードレビュー

この training() 関数は、Trainingsheet のデータを DBsheet に転記する処理を行っていますが、以下の点で改善

### **コードレビュー & 改善点**

この `training()` 関数は、`Trainingsheet` のデータを `DBsheet` に転記する処理を行っていますが、以下の点で改善できます。

## **🔹 改善点と修正方法**
### **1. `DBsheet` のデータ消去処理のバグ**
#### **🔹 問題点**

if(DB_lastrow!=2){
  DBsheet.getRange(3,1,DB_lastrow-2,DB_lastcolumn).clear();
}

– **`DB_lastrow != 2` のチェックが不適切**:
– `DBsheet` が空の場合 (`lastRow == 0` など)、`DBsheet.getRange(3,1,DB_lastrow-2,DB_lastcolumn)` の範囲指定でエラーが発生する可能性がある。
– `lastRow < 3` の場合 (`DBsheet` に2行未満のデータしかない場合)、`DBsheet.getRange(3,1,DB_lastrow-2,DB_lastcolumn)` の範囲が **負の値になりエラー** になる。 #### **✅ 改善策** [shell] if(DB_lastrow > 2){ DBsheet.getRange(3, 1, DB_lastrow - 2, DB_lastcolumn).clear(); } [/shell] - **`DB_lastrow > 2` の場合のみクリアする** ことで **エラー防止**。

### **2. `getValues()` を `getValues()` + `setValues()` に最適化**
#### **🔹 問題点**

DBsheet.getRange(3+count,1).setValue(Alldata[i][j+1]);//応答メッセージ
DBsheet.getRange(3+count,2).setValue(Alldata[i][0]);//キーワード

– **`setValue()` をループ内で何回も呼んでいる**(パフォーマンスが悪い)。
– **Google Apps Script は I/O 操作が遅い** ため、`getValues()` と `setValues()` を使って **一括処理** する方が高速。

#### **✅ 改善策**
– **データを `配列` に格納して、最後に `setValues()` で一括書き込み** する。

### **3. `Alldata[i][j+1]` の条件チェックを厳密に**
#### **🔹 問題点**

if(Alldata[i][j+1] != ""){

– `Alldata[i][j+1]` が `undefined` の場合 **比較が正しく機能しない可能性がある**。
– `null` や `空白スペース (” “)` のみのデータも無視した方がよい。

#### **✅ 改善策**

if (Alldata[i][j+1] && Alldata[i][j+1].trim() !== "") {

– `&& Alldata[i][j+1].trim() !== “”` を追加して、 **空白のみの値も無視** する。

## **🔹 改善後のコード**

function training() {
  var lastrow = Trainingsheet.getLastRow();
  var lastcolumn = Trainingsheet.getLastColumn();
  
  // Trainingシートのタイトル行を除く全データ取得
  var Alldata = Trainingsheet.getRange(2, 1, lastrow - 1, lastcolumn).getValues();
  
  var DB_lastrow = DBsheet.getLastRow();
  var DB_lastcolumn = DBsheet.getLastColumn();

  // DBシートのデータをクリア(タイトル行以降を削除)
  if (DB_lastrow > 2) {
    DBsheet.getRange(3, 1, DB_lastrow - 2, DB_lastcolumn).clear();
  }
  
  var dataToWrite = [];
  
  for (var i = 0; i < Alldata.length; i++) {
    for (var j = 0; j < Alldata[i].length - 1; j++) {
      if (Alldata[i][j+1] && Alldata[i][j+1].trim() !== "") {
        // データを配列に追加
        dataToWrite.push([Alldata[i][j+1], Alldata[i][0]]);
      }
    }
  }
  
  // まとめてDBシートに書き込み
  if (dataToWrite.length > 0) {
    DBsheet.getRange(3, 1, dataToWrite.length, 2).setValues(dataToWrite);
  }
}

## **🔹 改善点まとめ**
✅ **エラー防止:** `DB_lastrow > 2` のチェックを修正し、**負の範囲エラーを回避**
✅ **高速化:** `setValue()` の代わりに **配列にデータを溜めて `setValues()` で一括書き込み**
✅ **データチェックの厳密化:** `Alldata[i][j+1] && Alldata[i][j+1].trim() !== “”` で **空白や `undefined` を無視**
✅ **リファクタリング:** 変数名やロジックを **シンプルかつ分かりやすく** 変更

この修正により、処理速度と信頼性が向上し、エラーのリスクを減らせます!🚀

これを実行し再度デプロイ
URLが変更されるのでwebhookへ再度設定する

次に
replytouser.gs のコードレビュー

/**
 * 該当ユーザーへの
 * @param {*} replyToken 
 * @param {*} text 
 */
function replyToUser(replyToken, text) {

    const replyText = {
        "replyToken": replyToken,
        "messages": [{
            "type": "text",
            "text": text,
        }]
    }

    const options = {
        "method": "post",
        "headers":
        {
            "Content-Type": "application/json",
            "Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN,
        },
        "payload": JSON.stringify(replyText)
    };

    // Line該当ユーザーに応答を返している
    UrlFetchApp.fetch("https://api.line.me/v2/bot/message/reply", options);
}

この replyToUser() 関数は、LINE Bot を使って特定のユーザーに応答メッセージを送る処理を行っています。
基本的な構造は問題ありませんが、以下の改善点があり
### **コードレビュー & 改善点**

この `replyToUser()` 関数は、LINE Bot を使って特定のユーザーに応答メッセージを送る処理を行っています。
基本的な構造は問題ありませんが、以下の改善点があります。

## **🔹 改善点と修正方法**

### **1. `CHANNEL_ACCESS_TOKEN` の存在チェック**
#### **🔹 問題点**

"Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN,

– **`CHANNEL_ACCESS_TOKEN` が `null` または `undefined` の場合、エラーになる可能性がある**。
– もし `CHANNEL_ACCESS_TOKEN` の取得に失敗した場合、LINE API へのリクエストが **無駄に送信されてしまう**。

#### **✅ 改善策**
– `CHANNEL_ACCESS_TOKEN` の **存在を事前にチェック** し、エラーが発生する前に処理を停止する。

if (!CHANNEL_ACCESS_TOKEN) {
    throw new Error("CHANNEL_ACCESS_TOKEN が設定されていません");
}

### **2. `UrlFetchApp.fetch()` のエラーハンドリング**
#### **🔹 問題点**
– `UrlFetchApp.fetch()` で **LINE API へのリクエストが失敗してもエラーログが記録されない**。
– 例えば、ネットワークエラーや無効な `replyToken` の場合、原因が分からない。

#### **✅ 改善策**
– `try-catch` を使用してエラー発生時に `Logger.log()` でエラーメッセージを記録する。

try {
    const response = UrlFetchApp.fetch("https://api.line.me/v2/bot/message/reply", options);
    Logger.log("LINE API Response: " + response.getContentText());
} catch (error) {
    Logger.log("Error in replyToUser: " + error.toString());
}

### **3. LINE API のレスポンスをチェック**
#### **🔹 問題点**
– `UrlFetchApp.fetch()` のレスポンスを **無視している**。
– **エラーメッセージやステータスコードを確認しないと、何が問題か分からない**。

#### **✅ 改善策**
– `response.getResponseCode()` を使って **200(成功)以外の場合にログを記録する**。

const response = UrlFetchApp.fetch("https://api.line.me/v2/bot/message/reply", options);
const responseCode = response.getResponseCode();

if (responseCode !== 200) {
    Logger.log("LINE API Error: " + response.getContentText());
}

### **4. コメントの改善**
#### **🔹 問題点**
– 関数の説明コメントが **途中で途切れている**。
– `@param` の説明が不十分(`replyToken` の説明がない)。
– **関数が何をするのか明確に記述すべき**。

#### **✅ 改善策**
– **関数の説明を具体化** し、`@param` も明確に。

/**
 * LINE Bot で特定のユーザーにメッセージを送信する関数
 *
 * @param {string} replyToken - LINEから受け取った返信用トークン
 * @param {string} text - 送信するメッセージ内容
 */

## **🔹 改善後のコード**

/**
 * LINE Bot で特定のユーザーにメッセージを送信する関数
 *
 * @param {string} replyToken - LINEから受け取った返信用トークン
 * @param {string} text - 送信するメッセージ内容
 */
function replyToUser(replyToken, text) {
    // CHANNEL_ACCESS_TOKEN が設定されているか確認
    if (!CHANNEL_ACCESS_TOKEN) {
        throw new Error("CHANNEL_ACCESS_TOKEN が設定されていません");
    }

    const replyText = {
        "replyToken": replyToken,
        "messages": [{
            "type": "text",
            "text": text,
        }]
    };

    const options = {
        "method": "post",
        "headers": {
            "Content-Type": "application/json",
            "Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN,
        },
        "payload": JSON.stringify(replyText)
    };

    try {
        // LINE API へリクエストを送信
        const response = UrlFetchApp.fetch("https://api.line.me/v2/bot/message/reply", options);
        const responseCode = response.getResponseCode();

        // レスポンスをログに記録
        if (responseCode === 200) {
            Logger.log("LINE API Response: " + response.getContentText());
        } else {
            Logger.log("LINE API Error: " + response.getContentText());
        }
    } catch (error) {
        // エラーハンドリング
        Logger.log("Error in replyToUser: " + error.toString());
    }
}

## **🔹 改善点まとめ**
✅ **`CHANNEL_ACCESS_TOKEN` が `null` の場合にエラーをスロー**(事前チェック)
✅ **`try-catch` を追加し、`UrlFetchApp.fetch()` のエラーハンドリングを強化**
✅ **`response.getResponseCode()` をチェックし、API のレスポンスを記録**
✅ **コメント (`@param` の説明) を改善し、関数の目的を明確化**

この修正により、**エラーハンドリングが強化され、問題が発生したときにすぐ原因を特定できるようになります!** 🚀

再度デプロイして
webhookのURL更新

これでlinebot の動作確認できたので
過去に通知に line notify を使っていたものを調べて
Linebot に変える

リポジトリは
https://github.com/Snowpooll
にあるので

https://github.com/Snowpooll/store_adversting_lists
gmailで件名を指定し、未読の最新のメールを取得後にURLを抽出、
抽出したURLを元にブラウザを開き画像をダウンロード
ダウンロード画像へcloud vision api を実行し
購入リストにマッチしたものを LINE norifyで買い物リストが送信

https://github.com/Snowpooll/inventory_count
yoov8と学習したモデルで 指定した画像から物体検出をして在庫数が1以下になったときに
LINE norifyで買い物リストが送信
検出時の結果はsqlite3データベースに保存

とりあえずこの2つの機能を変えることにする