/* * Author : James Chou ( wiilands.com ) 教育版有AI_NAME、請安靜功能、關鍵字大小寫比對功能及30分鐘後自動回復說話, 所有的設定都可在Excel表和環境設定中完成,不要來動這程式,除非你會寫程式 */ var myWant; var mySearch; var uMessage; var userID; var userName; var userPicUrl; var keepSilent; var now; var userStyle; var userAskSilent; var userWords; var userHistory; var userLastTalks; var isKeyword; var isChatGPT var isToline var reply_message = []; //以下是環境變數,就是左方齒輪設定要先填才能代入 const env = { OPENAI_TOKEN: PropertiesService.getScriptProperties().getProperty('OPENAI_TOKEN'), CHANNEL_ACCESS_TOKEN: PropertiesService.getScriptProperties().getProperty('CHANNEL_ACCESS_TOKEN'), SHEET_URL:PropertiesService.getScriptProperties().getProperty('SHEET_URL'), AI_SET:PropertiesService.getScriptProperties().getProperty("AI_SET"), AI_MODEL:PropertiesService.getScriptProperties().getProperty("AI_MODEL"), AI_AFTER:PropertiesService.getScriptProperties().getProperty("AI_AFTER"), AI_NAME:PropertiesService.getScriptProperties().getProperty("AI_NAME")}; //以下是設定各工作表,下面程式要用 var sheet1 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("工作表1"); // 根據您的情況調整工作表名稱 var sheet2 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("客戶基本資料"); // 根據您的情況調整工作表名稱 var sheet3 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("關鍵字"); var sheet4 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("AI設定"); //下面兩行可以設定是否關閉ChatGPT和Line的回答,但要注意,關了Line,ChatGPT一定關閉,省錢,但只關了ChatGPT,Line仍然可用關鍵字應答. // isChatGPT = sheet4.getRange("B1").getValue(); // isToline = sheet4.getRange("B2").getValue(); // if (isToline != "yes") {isChatGPT = "no";sheet4.getRange("B1").setValue("No");} //確保 Line既然只記錄不輸出,自然不用ChatGPT了,省錢. isChatGPT = "yes"; //教育版專用 isToline = "yes"; //教育版專用 // 下面這主程式你就想像成,是使用者輸入時,Line回傳的資料,主要有訊息類別、誰傳的、傳了什麼字、還有一個叫replyToken的參數,這是用來回傳時,要比對的,不然電腦不曉得要回傳到哪. function doPost(e) { // LINE Messenging API Token // 以 JSON 格式解析 User 端傳來的 e 資料 var msg = JSON.parse(e.postData.contents); // for debugging Logger.log(msg); console.log(msg); // 從接收到的訊息中取出 replyToken 和發送的訊息文字 var replyToken = msg.events[0].replyToken; var myType = msg.events[0].message.type; var userMessage; if (myType !== "text") {userMessage="貼圖";} else { userMessage = msg.events[0].message.text;} // 以上的 if 是先判斷傳的是貼圖或文字,只要是文字,機器人才能據以回答,否則一律視為貼圖處理.(因為使用者有可能傳照片、聲音、影片等目前機器人還不能認出的物件) const user_id = msg.events[0].source.userId; var event_type = msg.events[0].source.type; try { var groupid = msg.events[0].source.groupId; } catch{ console.log("wrong"); } switch (event_type) { case "user": var nameurl = "https://api.line.me/v2/bot/profile/" + user_id; break; case "group": var nameurl = "https://api.line.me/v2/bot/group/" + groupid + "/member/" + user_id; break; } try { // 呼叫 LINE User Info API,以 user ID 取得該帳號的使用者名稱 var response = UrlFetchApp.fetch(nameurl, { "method": "GET", "headers": { "Authorization": "Bearer " + env.CHANNEL_ACCESS_TOKEN, "Content-Type": "application/json" }, }); var namedata = JSON.parse(response); var reserve_name = namedata.displayName ; var reserve_userId = namedata.userId; var reserve_pictureUrl = namedata.pictureUrl; var reserve_userMessage = userMessage; //以上我們最主要的就是取出 replyToken 、 userMessage、user_id、還有頭像的圖片網址 } catch{ reserve_name = "not avaliable"; } if (typeof replyToken === 'undefined') { return; }; userID = user_id; userName = reserve_name ; userPicUrl = reserve_pictureUrl; userWords = reserve_userMessage; now = Utilities.formatDate(new Date(), "GMT+8", "yyyy-MM-dd HH:mm:ss"); //只要是記錄,時間一定要加進來 var current_list_row = sheet1.getLastRow(); sheet1.getRange(current_list_row + 1, 1).setValue(reserve_userId); sheet1.getRange(current_list_row + 1, 2).setValue(reserve_name); sheet1.getRange(current_list_row + 1, 3).setValue(reserve_userMessage); sheet1.getRange(current_list_row + 1, 4).setValue(now); //以上程式,就算是關閉了關鍵字和ChatGPT回答,仍然保有記錄的功能 updateStatusBasedOnSilentTimeAndUserId(userID); //這是我加的新功能,有時機器人太多話,使用者可讓其安靜 if (reserve_userMessage.includes('請說話')||reserve_userMessage.includes('talking') ||reserve_userMessage.includes('Talking') ){beTalk(userID);} if (reserve_userMessage.includes('訂貨人姓名') && reserve_userMessage.includes('收貨人') ){beHistory(userID);} //這是記錄重要訂單 beLastTalks(); checkUserExist(); shouldRespond(userID); //先檢查要不要回應 var trimwords = userWords.trim(); if (trimwords.includes("請安靜")){userMessage="請安靜";} if (trimwords.includes("請說話")|| trimwords.includes("talking") || trimwords.includes("Talking")){userMessage="請說話";} mySearch = userMessage.toLowerCase().trim().replace(/[??嗎呢]/g, ''); userMessage = userMessage.toLowerCase().trim().replace(/[??嗎呢]/g, ''); if (userMessage.length < 2 && userMessage != "嗨") {mySearch = "文字太長或太短"; } if (userMessage.length > 300 ) {mySearch = "文字太長或太短"; } searchAndFormatResults(userMessage); //這是先用傳統的資料檢索,如果檢索的到,就不用啟動機器人,檢索不到,才啟動機器人 if (!myWant){ uMessage = ""; }else{ uMessage = myWant; isKeyword = "OK"; } if (uMessage!="") {myWant=env.AI_NAME+":"+myWant;reply_message = [{ "type": "text", "text": myWant}]} else { if (keepSilent != "安靜") { if (isChatGPT.toLowerCase().trim()== "yes") { let resp = GPTturbo(userMessage) //replyMessage = resp.slice(1).replace('\n\n\n','\n\n'); replyMessage = env.AI_NAME+":" + resp.replace('。',' \n'); replyMessage = replyMessage.replace('。',' \n'); reply_message = [{ "type": "text", "text": replyMessage}] }}} //回傳訊息給line 並傳送給使用者 if (isToline.trim().toLowerCase()== "yes") { if (keepSilent != "安靜" || isKeyword == "OK") { var url = 'https://api.line.me/v2/bot/message/reply'; UrlFetchApp.fetch(url, { 'headers': { 'Content-Type': 'application/json; charset=UTF-8', 'Authorization': 'Bearer ' + env.CHANNEL_ACCESS_TOKEN, }, 'method': 'post', 'payload': JSON.stringify({ 'replyToken': replyToken, 'messages': reply_message, }), })};} if (reserve_userMessage.includes('請安靜')){beSilent(userID);} } //以下是各副程式,或叫function function GPTturbo(prompt, temperature = 0.4, model = env.AI_MODEL) { const MAX_TOKENS = 800; const TEMPERATURE = 0.4; const url = "https://api.openai.com/v1/chat/completions"; const payload = { model: model, messages: [ { role: "system", content: env.AI_SET}, { role: "user", content: prompt }, ], temperature: TEMPERATURE, max_tokens: MAX_TOKENS, }; const options = { contentType: "application/json", headers: { Authorization: "Bearer " + env.OPENAI_TOKEN }, payload: JSON.stringify(payload), }; const res = JSON.parse(UrlFetchApp.fetch(url, options).getContentText()); var asd= res.choices[0].message.content.trim().replace('。',' \n'); if (asd.includes("I will keep quite")) {beSilent();} if (asd == "on") {beTalk();} return res.choices[0].message.content.trim().replace('。',' \n'); } function checkUserExist() { // 檢查客戶基本資料表中UserID是否存在 var basicDataRange = sheet2.getRange(2, 1, sheet2.getLastRow(), 1); var basicDataValues = basicDataRange.getValues(); var userExists = false; for (var i = 0; i < basicDataValues.length; i++) { if (basicDataValues[i][0] == userID) { userExists = true; userRow = i + 2; // 因為範圍是從第二行開始的 break; } } // 如果用戶不存在於客戶基本資料表中,則新增 if (!userExists) { sheet2.appendRow([userID, userName,userPicUrl,now,"說話","預設風格"]); userRow = sheet2.getLastRow(); // 獲取新添加的行數 } } function shouldRespond(userId) { const data = sheet2.getDataRange().getValues(); const userRow = data.find(row => row[0] === userId); // 假设 UserID 在第1 Column if (userRow) {userStyle = userRow[5];userHistory = userRow[6];userLastTalks = userRow[8];} if (userRow && userRow[4] === "安靜") { keepSilent = "安靜"; return false; // ReplyState 是 'Silent',不回答 } return true; // ReplyState 是空白,可以回答 } function beSilent() { // 檢查客戶基本資料表中UserID是否存在 var basicDataRange = sheet2.getRange(2, 1, sheet2.getLastRow(), 1); var basicDataValues = basicDataRange.getValues(); for (var i = 0; i < basicDataValues.length; i++) { if (basicDataValues[i][0] == userID) { userExists = true; sheet2.getRange(i + 2, 5).setValue("安靜"); sheet2.getRange(i + 2, 11).setValue(now); userRow = i + 2; // 因為範圍是從第二行開始的 break; } } } function beTalk() { // 檢查客戶基本資料表中UserID是否存在 var basicDataRange = sheet2.getRange(2, 1, sheet2.getLastRow(), 1); var basicDataValues = basicDataRange.getValues(); for (var i = 0; i < basicDataValues.length; i++) { if (basicDataValues[i][0] == userID) { userExists = true; sheet2.getRange(i + 2, 5).setValue("說話"); userRow = i + 2; // 因為範圍是從第二行開始的 break; } } } function beHistory() { // 检查客戶基本资料表中UserID是否存在 var basicDataRange = sheet2.getRange(2, 1, sheet2.getLastRow(), 1); var basicDataValues = basicDataRange.getValues(); for (var i = 0; i < basicDataValues.length; i++) { if (basicDataValues[i][0] == userID) { // 获取原有的值,并将换行符替换为空格 var originalValue = sheet2.getRange(i + 2, 7).getValue().replace(/\n/g, " "); // 将新值中的换行符也替换为空格,然后拼接 var newValue = originalValue+";" + userWords.replace(/\n/g, " "); // 检查newValue的长度是否超过1000个字符 if (newValue.length > 1000) { // 如果超过1000个字符,从原始值中去掉最前面的100个字符 // 然后再加上新值 newValue = originalValue.substring(100) + userWords.replace(/\n/g, " "); } // 如果newValue长度加上新值还是超过1000个字符,继续从前面切除直到小于1000个字符 while (newValue.length > 1000) { newValue = newValue.substring(100); } // 更新单元格的值 sheet2.getRange(i + 2, 7).setValue(newValue); sheet2.getRange(i + 2, 8).setValue(now); break; } } } function beLastTalks() { // 检查客戶基本资料表中UserID是否存在 var basicDataRange = sheet2.getRange(2, 1, sheet2.getLastRow(), 1); var basicDataValues = basicDataRange.getValues(); for (var i = 0; i < basicDataValues.length; i++) { if (basicDataValues[i][0] == userID) { // 获取原有的值,并将换行符替换为空格 var originalValue = sheet2.getRange(i + 2, 9).getValue().replace(/\n/g, " "); // 将新值中的换行符也替换为空格,然后拼接 var newValue = originalValue+";" + userWords.replace(/\n/g, " "); // 检查newValue的长度是否超过1000个字符 if (newValue.length > 1000) { // 如果超过1000个字符,从原始值中去掉最前面的100个字符 // 然后再加上新值 newValue = originalValue.substring(100) + userWords.replace(/\n/g, " "); } // 如果newValue长度加上新值还是超过1000个字符,继续从前面切除直到小于1000个字符 while (newValue.length > 1000) { newValue = newValue.substring(100); } // 更新单元格的值 sheet2.getRange(i + 2, 9).setValue(newValue); sheet2.getRange(i + 2,10).setValue(now); break; } } } function searchAndFormatResults(searchTerm) { //searchTerm = "上課地點"; // 检查搜索词的有效性 if ((searchTerm.length < 2 && !/[a-zA-Z]/.test(searchTerm)) || searchTerm.length === 0) { return "搜索词至少需要两个字符(对于中文),或者是英文字符。"; } var range = sheet3.getDataRange(); var values = range.getValues(); var textResult = ""; // 遍历每一行 for (var i = 0; i < values.length; i++) { // 检查第一列是否包含搜索词 if (values[i][0].toString().toLowerCase().trim().includes(searchTerm.toLowerCase().trim())) { // 如果第一列匹配,将第二列的值加入结果 var rowText = appendIfNotEmpty(values[i][1]); rowText = rowText.replace(/undefined/g,""); rowText = rowText.replace(/\n\n/g,"\n"); textResult += rowText + "\n"; // 每个匹配结果后加上换行符 } } // 检查是否有匹配结果 if (textResult === "") { Logger.log(textResult.trim()); } else { // 日志输出匹配结果,或根据需要处理匹配结果 Logger.log("結果:"+textResult.trim()); myWant = textResult.trim(); // 返回纯文本格式的匹配结果 } } //下面的 function 是30分鐘後主動回復機器人答話,不然有些使用者忘了曾關閉機器人 function updateStatusBasedOnSilentTimeAndUserId(specificUserId) { var range = sheet2.getDataRange(); // 获取工作表中的所有数据范围 var values = range.getValues(); // 获取范围内所有值的二维数组 var currentTime = Utilities.formatDate(new Date(), "GMT+8", "yyyy-MM-dd HH:mm:ss"); // 获取当前时间 var timeUnix = datetimeToUnix(currentTime); Logger.log(timeUnix); // 遍历每行数据 for (var i = 1; i < values.length; i++) { // 从第二行开始,假设第一行是标题行 var row = values[i]; var userId = row[0]; // UserID列的索引为0 var replyState = row[4]; // ReplyState列的索引为4 var silentTimeStr = row[10]; // SilentTime列的索引为10 var timeUnix2 = datetimeToUnix(silentTimeStr); Logger.log(timeUnix2); // 确保UserID匹配,状态为"安靜",且SilentTime列有值 if (userId === specificUserId && replyState == "安靜" && silentTimeStr) { var timeDiff = (timeUnix - timeUnix2) / 60; // 计算时间差异,转换为分钟 Logger.log(timeDiff); if (timeDiff > 30) { sheet2.getRange(i + 1, 5).setValue("說話"); // 如果超过30分钟,更新ReplyState为"說話" Logger.log("成功") } } } } function datetimeToUnix(timestampStr) { // 将日期字符串转换为Date对象 var date = new Date(timestampStr); // 获取UNIX时间戳(Date对象的getTime()方法返回毫秒,所以除以1000转换为秒) var unixTimestamp = date.getTime() / 1000; return unixTimestamp; } function appendIfNotEmpty(value) { // 检查值是否为 undefined 或空字符串(包括空白字符) if (value !== undefined && value !== null && value.trim() !== '') { return value + "\n"; } return ""; }