☁️ LINE Bot 開發懶人包 (GAS + Gemini 版)
還在煩惱 LINE Bot 開發 需要昂貴的伺服器費用嗎?透過 Google Apps Script (GAS) 結合最新的 Gemini API,您可以完全免費打造強大的 AI 助理。以下是本教學核心亮點:
| 組件 | 功能與優勢 |
|---|---|
| LINE Bot 開發 (Messaging API) | 提供使用者介面,支援 Flex Message 精美卡片互動。 |
| Google Apps Script (GAS) | 免維護伺服器 (Serverless),完全免費,直接操作 Google Sheet 資料庫。 |
| Gemini API | Google 最新 AI 模型,具備長文本理解與高速回應能力,每月有免費額度。 |
| AI 助理功能 | 具備行程管理、待辦事項分類、自動晨報與自然語言對話能力。 |
什麼是 LINE Bot 開發?核心架構與原理
LINE Bot 開發 是透過 Messaging API 將 LINE 官方帳號連接至後端伺服器的技術。本教學使用 Google Apps Script 取代傳統伺服器,大幅降低開發門檻與成本。
我們在克隆資訊實驗室測試了多種 LINE Bot 開發 架構,發現對於個人或中小企業而言,使用 Google Apps Script (GAS) 搭配 Google Sheets 作為資料庫,是 CP 值最高的解決方案。這種架構不僅免費,還能無縫串接 Google 生態系服務。
技術架構圖解
本專案的運作流程如下,這也是目前最流行的 Serverless 開發模式:
前端:LINE App (用戶輸入自然語言,如「明天下午三點開會」)。
- 橋樑:Messaging API 接收訊息並轉發。
- 核心:Google Apps Script 接收 Webhook,進行邏輯處理。
- 腦袋:Gemini API 解析語意,判斷是「行程」、「待辦」還是「閒聊」。
- 記憶:Google Sheets 儲存資料。
LINE Bot 開發事前準備:帳號與環境設定
開始開發前,您需要準備 LINE Developers 帳號、Google Cloud 專案以取得 Gemini API Key,以及一個 Google 帳號用於存取 Apps Script。
Step 1: 取得 LINE Channel (Messaging API)
這是 LINE Bot 開發 的第一步,我們需要一個「身分證」讓程式與 LINE 溝通。
- 註冊 LINE Developers:前往 LINE Developers,使用您的 LINE 帳號登入。
- 建立 Provider:點擊「Create」→「Provider name」,輸入名稱(例如:個人 AI 專案)。
- 建立 Channel:
- 選擇「Create a Messaging API channel」。
- 會先要求新增官方帳號點擊「Create a LINE Offcial Account」
- 建立LINE官方帳號 頁面 在依照自記填入資料
- 您的LINE官方帳號已建立完成後選擇「稍後進行認證 ( 前往管理畫面 )」
- 進入LINE官方帳號管理後台選擇「設定」>「Messaging API」>「啟用Messaging API」
- 取得關鍵金鑰 (請存入記事本):
- 「啟用Messaging API」後頁面會顯示「Channel secret」複製起來
- 回到 LINE Developers 選擇剛剛建立的「Provider」與「Channels」
- 到「Messaging API」頁籤,點擊下面 Channel access token「lssue」。
- (會出現很長一串亂碼這就是 Channel Access Token 複製起來)

Step 2: 取得 Gemini API Key
為了讓您的 AI 助理 變聰明,我們需要 Google Gemini 模型的支援。
- 前往 Google AI Studio。
- 點擊「Create API key」→「Create API key in new project」。
- 複製生成的 API Key (開頭通常是 AIzaSy…)。

Step 3: 建立 Google Sheet 資料庫
我們不需要 SQL 資料庫,強大的 Google Sheet 就足夠了。
前往 sheets.new 建立新試算表,命名為「AI 助理資料庫」。
Google Apps Script 核心實戰:程式碼部署
透過 Google Apps Script,我們可以將 LINE Messaging API 與 Gemini API 串接。請依照下方結構建立四個 .gs 檔案,這是 LINE Bot 開發 中最關鍵的邏輯層。
在 Google Sheet 中點擊「擴充功能」→「Apps Script」,並建立以下檔案結構。
1. Code.gs (主程式邏輯)

這是 LINE Bot 開發 的入口,負責處理 Webhook 請求。
// ===== 主要進入點 =====
function doPost(e) {
try {
const events = JSON.parse(e.postData.contents).events;
events.forEach(event => {
if (event.type === 'message' && event.message.type === 'text') {
handleMessage(event);
} else if (event.type === 'follow') {
handleNewUser(event);
}
});
} catch (error) {
Logger.log('Error: ' + error);
}
return HtmlService.createHtmlOutput('OK');
}
function doGet(e) {
return HtmlService.createHtmlOutput('✅ AI 助理運作中!');
}
// ===== 新用戶歡迎 =====
function handleNewUser(event) {
ensureDataExists();
const welcomeMessage = `👋 歡迎使用 AI 助理!
我可以幫你:
📅 記錄行程(自動提醒)
✅ 管理待辦清單(AI智能分類)
📝 儲存備忘錄
📊 每日晨報 & 週報總結
快速開始:
「明天3點開會」
「記得要寄包裹」
「記一下客戶電話0912345678」
「今天有什麼事」
「本週行程」
讓我幫你管理生活!🚀`;
replyToLine(event.replyToken, welcomeMessage);
}
// ===== 處理訊息 =====
function handleMessage(event) {
const userMessage = event.message.text;
const replyToken = event.replyToken;
const userId = event.source.userId;
ensureDataExists();
saveUserId(userId);
// ===== 快速指令(不用 AI,直接處理)=====
// 完成行程 ⭐ 新增
if (userMessage.startsWith('完成行程#')) {
const match = userMessage.match(/#(\d+)/);
if (match) {
const result = completeEvent(match[1]);
replyToLine(replyToken, result.message);
return;
}
}
// 待辦清單
if (userMessage === '待辦清單' || userMessage === '待辦' || userMessage === 'todo' || userMessage === 'TODO') {
const todos = getTodoList();
replyTodoListCard(replyToken, todos);
return;
}
// 備忘錄
if (userMessage === '備忘錄' || userMessage === '筆記' || userMessage === '備註' || userMessage === 'memo') {
const memos = getMemoList();
replyMemoListCard(replyToken, memos);
return;
}
// 今天
if (userMessage === '今天' || userMessage === '今日' || userMessage === '今天有什麼事') {
const events = getTodayEvents();
replyTodayEventsCard(replyToken, events);
return;
}
// 明天
if (userMessage === '明天' || userMessage === '明天的行程') {
const events = getTomorrowEvents();
replyDayEventsCard(replyToken, events, '明天');
return;
}
// 後天
if (userMessage === '後天') {
const events = getDayAfterTomorrowEvents();
replyDayEventsCard(replyToken, events, '後天');
return;
}
// 本週
if (userMessage === '本週' || userMessage === '本周' || userMessage === '這週' || userMessage === '這周' || userMessage === '本週行程') {
const events = getThisWeekEvents();
replyWeekEventsCard(replyToken, events);
return;
}
// 本月
if (userMessage === '本月' || userMessage === '這個月' || userMessage === '本月行程') {
const events = getThisMonthEvents();
replyMonthEventsCard(replyToken, events);
return;
}
// 全部
if (userMessage === '全部' || userMessage === '全部行程' || userMessage === '所有行程') {
const events = getAllEvents();
replyAllEventsCard(replyToken, events);
return;
}
// 完成待辦
if (userMessage.startsWith('完成#') || userMessage.startsWith('做完#')) {
const match = userMessage.match(/#(\d+)/);
if (match) {
const result = completeTodo(match[1]);
replyToLine(replyToken, result.message);
return;
}
}
// 刪除待辦 ⭐ 新增
if (userMessage.startsWith('刪除待辦#') || userMessage.startsWith('刪待辦#')) {
const match = userMessage.match(/#(\d+)/);
if (match) {
const result = deleteTodo(match[1]);
replyToLine(replyToken, result.message);
return;
}
}
// 刪除行程 ⭐ 修改
if (userMessage.startsWith('取消行程#') || userMessage.startsWith('刪除行程#') || userMessage.startsWith('刪行程#')) {
const match = userMessage.match(/#(\d+)/);
if (match) {
const result = deleteEvent(match[1]);
replyToLine(replyToken, result.message);
return;
}
}
// 刪除備忘錄 ⭐ 新增
if (userMessage.startsWith('刪除備忘#') || userMessage.startsWith('刪備忘#')) {
const match = userMessage.match(/#(\d+)/);
if (match) {
const result = deleteMemo(match[1]);
replyToLine(replyToken, result.message);
return;
}
}
// 幫助
if (userMessage === '幫助' || userMessage === '說明' || userMessage === 'help' || userMessage === '?' || userMessage === '?') {
const helpMessage = `📚 AI 助理使用說明
📅 行程管理:
- 「明天3點開會」
- 「今天下午2點報告、3點討論、4點吃飯」
- 「1月10號早上9點會議」
✅ 待辦清單:
- 「記得要寄包裹」
- 「要買菜」
- 輸入「待辦清單」查看
- 完成:「完成#編號」
- 刪除:「刪除待辦#編號」
📝 備忘錄:
- 「記一下客戶電話0912345678」
- 輸入「備忘錄」查看
- 刪除:「刪除備忘#編號」
🔍 查詢行程:
- 今天、明天、後天
- 本週、本月、全部
- 1/10、1月10號
- 刪除:「刪除行程#編號」
⚡ 快速指令:
- 待辦清單、備忘錄
- 完成#編號
- 刪除待辦#編號
- 刪除行程#編號
- 刪除備忘#編號
有問題隨時問我!😊`;
replyToLine(replyToken, helpMessage);
return;
}
// ===== 其他情況交給 AI 處理 =====
const aiResponse = callGemini(userMessage);
switch(aiResponse.intent) {
case 'add_event':
const eventResult = addEvent(aiResponse.parameters);
replyEventSuccessCard(replyToken, eventResult);
break;
case 'add_events':
const eventsResults = addMultipleEvents(aiResponse.parameters.events);
replyMultipleEventsSuccessCard(replyToken, eventsResults);
break;
case 'add_todo':
const todoResult = addTodo(aiResponse.parameters);
replyTodoSuccessCard(replyToken, todoResult);
break;
case 'add_memo':
const memoResult = addMemo(aiResponse.parameters);
replyMemoSuccessCard(replyToken, memoResult);
break;
case 'query_schedule':
handleScheduleQuery(replyToken, aiResponse.parameters);
break;
case 'complete_todo':
const completeResult = completeTodo(aiResponse.parameters.todo_id);
replyToLine(replyToken, completeResult.message);
break;
case 'delete_event':
const deleteResult = deleteEvent(aiResponse.parameters.event_id);
replyToLine(replyToken, deleteResult.message);
break;
case 'general_chat':
replyToLine(replyToken, aiResponse.reply_text);
break;
default:
replyToLine(replyToken,
'我可以幫你:\n' +
'📅 記錄行程:「明天3點開會」\n' +
'✅ 待辦事項:「記得要寄包裹」\n' +
'📝 備忘錄:「記一下電話號碼」\n' +
'🔍 查詢:「今天」「待辦清單」「備忘錄」\n\n' +
'輸入「幫助」查看完整說明'
);
}
}
// ===== 處理行程查詢 =====
function handleScheduleQuery(replyToken, params) {
const queryType = params.query_type;
switch(queryType) {
case 'today':
const todayEvents = getTodayEvents();
replyTodayEventsCard(replyToken, todayEvents);
break;
case 'tomorrow':
const tomorrowEvents = getTomorrowEvents();
replyDayEventsCard(replyToken, tomorrowEvents, '明天');
break;
case 'day_after_tomorrow':
const dayAfterEvents = getDayAfterTomorrowEvents();
replyDayEventsCard(replyToken, dayAfterEvents, '後天');
break;
case 'this_week':
const weekEvents = getThisWeekEvents();
replyWeekEventsCard(replyToken, weekEvents);
break;
case 'this_month':
const monthEvents = getThisMonthEvents();
replyMonthEventsCard(replyToken, monthEvents);
break;
case 'all':
const allEvents = getAllEvents();
replyAllEventsCard(replyToken, allEvents);
break;
case 'specific_date':
const specificEvents = getSpecificDateEvents(params.specific_date);
replyDayEventsCard(replyToken, specificEvents, params.specific_date);
break;
default:
replyToLine(replyToken, '請告訴我要查詢哪天的行程');
}
}
// ===== 自動提醒、晨報、週報(保持不變)=====
function checkReminders() {
const userIds = getAllUserIds();
if (userIds.length === 0) {
Logger.log('沒有用戶資料');
return;
}
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('行事曆');
const data = sheet.getDataRange().getValues();
const now = new Date();
for (let i = 1; i < data.length; i++) {
let eventDate = data[i][1];
let eventTime = data[i][2];
const reminded = data[i][4];
const status = data[i][5];
if (typeof eventDate === 'string') {
eventDate = eventDate;
} else if (eventDate instanceof Date) {
eventDate = Utilities.formatDate(eventDate, 'GMT+8', 'yyyy/MM/dd');
}
if (eventTime instanceof Date) {
eventTime = Utilities.formatDate(eventTime, 'GMT+8', 'HH:mm');
} else if (typeof eventTime === 'string') {
eventTime = eventTime;
} else {
eventTime = String(eventTime);
}
const eventDateTime = new Date(eventDate + ' ' + eventTime);
if (reminded !== '已提醒' && status === '待辦') {
const timeDiff = eventDateTime - now;
if (timeDiff > 0 && timeDiff <= 30 * 60000) {
const message = `⏰ 提醒!\n\n30分鐘後有行程\n📅 ${eventDate} ${eventTime}\n📝 ${data[i][3]}`;
userIds.forEach(userId => {
pushToLine(userId, message);
});
sheet.getRange(i + 1, 5).setValue('已提醒');
Logger.log(`已發送提醒: ${data[i][3]}`);
}
}
}
}
function dailyReport() {
const userIds = getAllUserIds();
if (userIds.length === 0) {
Logger.log('沒有用戶資料');
return;
}
const report = generateDailyReport();
userIds.forEach(userId => {
pushToLine(userId, report);
});
Logger.log('每日晨報已發送');
}
function weeklyReport() {
const userIds = getAllUserIds();
if (userIds.length === 0) {
Logger.log('沒有用戶資料');
return;
}
const report = generateWeeklyReport();
userIds.forEach(userId => {
pushToLine(userId, report);
});
Logger.log('週報總結已發送');
}
function generateDailyReport() {
const today = Utilities.formatDate(new Date(), 'GMT+8', 'yyyy/MM/dd (E)');
const events = getTodayEvents();
const todos = getTodoList();
let message = `☀️ 早安!${today}\n\n`;
if (events.length > 0) {
message += `📅 今日行程(${events.length}項)\n`;
events.forEach(e => {
message += `⏰ ${e.time} ${e.item}\n`;
});
message += '\n';
}
if (todos.length > 0) {
message += `✅ 待辦事項(${todos.length}項)\n`;
const grouped = {};
todos.forEach(t => {
const category = t.category || '一般';
if (!grouped[category]) grouped[category] = [];
grouped[category].push(t);
});
Object.keys(grouped).forEach(category => {
message += `\n【${category}】\n`;
grouped[category].slice(0, 3).forEach(t => {
message += `• ${t.item}\n`;
});
if (grouped[category].length > 3) {
message += `...還有${grouped[category].length - 3}項\n`;
}
});
message += '\n';
}
if (events.length === 0 && todos.length === 0) {
message += '今天沒有安排,享受自由時光!😊';
} else {
message += '祝你有美好的一天!💪';
}
return message;
}
function generateWeeklyReport() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('待辦清單');
const data = sheet.getDataRange().getValues();
const now = new Date();
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
let completedCount = 0;
let pendingCount = 0;
const completedByCategory = {};
for (let i = 1; i < data.length; i++) {
const status = data[i][4];
const category = data[i][3] || '一般';
const completedTime = data[i][5] ? new Date(data[i][5]) : null;
if (status === '已完成' && completedTime && completedTime >= weekAgo) {
completedCount++;
completedByCategory[category] = (completedByCategory[category] || 0) + 1;
}
if (status === '待辦') {
pendingCount++;
}
}
let message = `📊 本週總結\n\n`;
message += `✅ 完成事項:${completedCount} 項\n`;
message += `⏳ 待辦事項:${pendingCount} 項\n\n`;
if (completedCount > 0) {
message += `📈 完成分布:\n`;
Object.keys(completedByCategory).forEach(category => {
message += ` ${category}:${completedByCategory[category]} 項\n`;
});
message += '\n';
}
if (completedCount >= 10) {
message += `🏆 太棒了!完成了${completedCount}項任務!\n`;
} else if (completedCount >= 5) {
message += `👍 不錯!完成了${completedCount}項任務!\n`;
} else if (completedCount > 0) {
message += `💪 繼續努力!已完成${completedCount}項!\n`;
} else {
message += `🌟 下週一起加油!\n`;
}
if (pendingCount > 0) {
message += `\n還有${pendingCount}項待辦,繼續保持!`;
}
return message;
}
2. GeminiAPI.gs (AI 大腦串接)
此段代碼是 AI 助理 的核心,負責理解自然語言並轉換為 JSON 指令。
// ===== 呼叫 Gemini AI =====
function callGemini(userMessage) {
const apiKey = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
if (!apiKey) {
return {
intent: 'general_chat',
reply_text: '系統設定錯誤'
};
}
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${apiKey}`;
const recentTodos = getTodoList().slice(0, 5);
const upcomingEvents = getUpcomingEvents(3);
const contextInfo = `
最近待辦:
${recentTodos.map(t => `#${t.id} [${t.category}] ${t.item}`).join('\n') || '無'}
近期行程:
${upcomingEvents.map(e => `${e.date} ${e.time} ${e.item}`).join('\n') || '無'}
`;
const systemPrompt = `你是我的私人 AI 助理,請用繁體中文、專業且友善的語氣回應。
當前時間:${Utilities.formatDate(new Date(), 'GMT+8', 'yyyy/MM/dd HH:mm')}
星期:${['日','一','二','三','四','五','六'][new Date().getDay()]}
${contextInfo}
你的任務是分析我的訊息,判斷意圖,並以 JSON 格式回應(不要有 markdown 標記):
{
"intent": "add_event" | "add_events" | "add_todo" | "add_memo" | "query_schedule" | "complete_todo" | "delete_event" | "general_chat",
"parameters": {
"date": "YYYY/MM/DD 格式",
"time": "HH:mm 格式(24小時制)",
"item": "事項內容",
"events": [
{"date": "YYYY/MM/DD", "time": "HH:mm", "item": "事項1"},
{"date": "YYYY/MM/DD", "time": "HH:mm", "item": "事項2"}
],
"content": "備忘錄內容",
"category": "工作|購物|財務|溝通|學習|健康|娛樂|家務|其他",
"query_type": "today|tomorrow|day_after_tomorrow|this_week|this_month|all|specific_date",
"specific_date": "當 query_type=specific_date 時,填入 YYYY/MM/DD",
"todo_id": "待辦編號",
"event_id": "行程編號"
},
"reply_text": "回覆內容(僅 general_chat 時填寫)"
}
意圖判斷規則:
1. **add_event**(新增單個行程)
觸發詞:明天、下週、幾點、開會、面試、聚餐、約、會議等
範例:
- 「明天3點開會」→ date: 明天日期, time: "15:00", item: "開會"
2. **add_events**(新增多個行程)⭐ 新增!
當用戶一次說多個行程時使用此意圖
範例:
- 「今天下午3點吃點心、4點摸魚文章、5點發布查詢、6點逛逛商務」
→ events: [
{"date": "2026/01/07", "time": "15:00", "item": "吃點心"},
{"date": "2026/01/07", "time": "16:00", "item": "摸魚文章"},
{"date": "2026/01/07", "time": "17:00", "item": "發布查詢"},
{"date": "2026/01/07", "time": "18:00", "item": "逛逛商務"}
]
- 「明天2點報告、3點開會、4點吃飯」
→ events: [
{"date": "明天日期", "time": "14:00", "item": "報告"},
{"date": "明天日期", "time": "15:00", "item": "開會"},
{"date": "明天日期", "time": "16:00", "item": "吃飯"}
]
**重要:當偵測到多個時間點或多個事項時,必須使用 add_events 而不是 add_event**
時間解析:
- 早上/上午 = 09:00-11:00
- 中午 = 12:00
- 下午 = 14:00-17:00(如果只說「下午3點」=15:00)
- 傍晚 = 18:00
- 晚上 = 19:00-21:00
3. **add_todo**(新增待辦)
觸發詞:記得、要做、待辦、要買、要繳、要寄等
範例:
- 「記得要寄包裹」→ item: "寄包裹", category: "家務"
**重要:必須智能判斷 category 類別**
4. **add_memo**(新增備忘錄)
觸發詞:記一下、備註、筆記、記住、保存等
範例:
- 「記一下客戶電話0912345678」→ content: "客戶電話0912345678"
5. **query_schedule**(查詢行程)
觸發詞:今天、明天、後天、本週、本月、全部、幾號等
範例:
- 「今天」或「今天有什麼事」→ query_type: "today"
- 「明天」或「明天的行程」→ query_type: "tomorrow"
- 「本週」或「這週行程」→ query_type: "this_week"
- 「1/10」或「1月10號」→ query_type: "specific_date", specific_date: "2026/01/10"
6. **complete_todo**(完成待辦)
範例:「完成#3」→ todo_id: "3"
7. **delete_event**(刪除行程)
範例:「取消#5」→ event_id: "5"
8. **general_chat**(一般對話)
問候、感謝、詢問功能等
請務必回傳純 JSON,不要有任何其他文字。`;
const payload = {
contents: [{
parts: [{
text: `${systemPrompt}\n\n我的訊息:${userMessage}`
}]
}]
};
const options = {
method: 'POST',
contentType: 'application/json',
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
try {
const response = UrlFetchApp.fetch(url, options);
const statusCode = response.getResponseCode();
const responseText = response.getContentText();
Logger.log('Gemini HTTP Status: ' + statusCode);
if (statusCode !== 200) {
Logger.log('Gemini API 錯誤: ' + responseText);
return {
intent: 'general_chat',
reply_text: '系統暫時繁忙,請稍後再試!'
};
}
const result = JSON.parse(responseText);
if (!result.candidates || !result.candidates[0] || !result.candidates[0].content) {
return {
intent: 'general_chat',
reply_text: '抱歉,我沒聽懂,可以再說一次嗎?'
};
}
const aiText = result.candidates[0].content.parts[0].text;
Logger.log('Gemini 原始回應: ' + aiText);
let cleanedText = aiText
.replace(/```json\n?|\n?```/g, '')
.replace(/^[^{]*/g, '')
.replace(/[^}]*$/g, '')
.trim();
try {
const parsedResult = JSON.parse(cleanedText);
Logger.log('解析結果: ' + JSON.stringify(parsedResult));
return parsedResult;
} catch (parseError) {
Logger.log('JSON 解析失敗: ' + parseError);
return {
intent: 'general_chat',
reply_text: '抱歉,我沒聽懂,可以再說一次嗎?'
};
}
} catch (error) {
Logger.log('Gemini 調用失敗: ' + error);
return {
intent: 'general_chat',
reply_text: '系統暫時繁忙,請稍後再試!'
};
}
}
3. Database.gs (Google Sheet 操作)
負責實際寫入與讀取試算表。
// ===== 儲存 User ID =====
function saveUserId(userId) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('用戶資料');
const data = sheet.getDataRange().getValues();
// 檢查是否已存在
for (let i = 1; i < data.length; i++) {
if (data[i][0] === userId) {
// 更新最後活躍時間
sheet.getRange(i + 1, 3).setValue(new Date());
return;
}
}
// 不存在則新增
const now = Utilities.formatDate(new Date(), 'GMT+8', 'yyyy/MM/dd HH:mm');
sheet.appendRow([userId, '用戶', now]);
Logger.log('新用戶已儲存: ' + userId);
}
// ===== 獲取所有 User ID =====
function getAllUserIds() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('用戶資料');
const data = sheet.getDataRange().getValues();
const userIds = [];
for (let i = 1; i < data.length; i++) {
if (data[i][0]) {
userIds.push(data[i][0]);
}
}
return userIds;
}
// ===== 新增行程 =====
function addEvent(params) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('行事曆');
const lastRow = sheet.getLastRow();
const newId = lastRow > 1 ? sheet.getRange(lastRow, 1).getValue() + 1 : 1;
const date = params.date || '';
const time = params.time || '';
const item = params.item || '';
sheet.appendRow([
newId,
date,
time,
item,
'未提醒',
'待辦'
]);
return {
success: true,
id: newId,
date: date,
time: time,
item: item
};
}
// ===== 新增待辦 =====
function addTodo(params) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('待辦清單');
const lastRow = sheet.getLastRow();
const newId = lastRow > 1 ? sheet.getRange(lastRow, 1).getValue() + 1 : 1;
const item = params.item || '';
const category = params.category || '其他';
const now = Utilities.formatDate(new Date(), 'GMT+8', 'yyyy/MM/dd HH:mm');
sheet.appendRow([
newId,
item,
now,
category,
'待辦',
''
]);
return {
success: true,
id: newId,
item: item,
category: category
};
}
// ===== 新增備忘錄 =====
function addMemo(params) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('備忘錄');
const lastRow = sheet.getLastRow();
const newId = lastRow > 1 ? sheet.getRange(lastRow, 1).getValue() + 1 : 1;
const content = params.content || '';
const now = Utilities.formatDate(new Date(), 'GMT+8', 'yyyy/MM/dd HH:mm');
sheet.appendRow([
newId,
content,
now
]);
return {
success: true,
id: newId,
content: content
};
}
// ===== 完成待辦 =====
function completeTodo(id) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('待辦清單');
const data = sheet.getDataRange().getValues();
for (let i = 1; i < data.length; i++) {
if (data[i][0] == id) {
const item = data[i][1];
const category = data[i][3];
const now = Utilities.formatDate(new Date(), 'GMT+8', 'yyyy/MM/dd HH:mm');
sheet.getRange(i + 1, 5).setValue('已完成');
sheet.getRange(i + 1, 6).setValue(now);
return {
success: true,
message: `🎉 太棒了!\n\n✅ 已完成 [${category}]:${item}`
};
}
}
return {
success: false,
message: `❌ 找不到編號 #${id} 的待辦事項`
};
}
// ===== 刪除行程 =====
function deleteEvent(id) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('行事曆');
const data = sheet.getDataRange().getValues();
for (let i = 1; i < data.length; i++) {
if (data[i][0] == id) {
const item = data[i][3];
sheet.deleteRow(i + 1);
return {
success: true,
message: `✅ 已刪除行程\n\n📝 ${item}`
};
}
}
return {
success: false,
message: `❌ 找不到編號 #${id} 的行程`
};
}
// ===== 獲取待辦清單 =====
function getTodoList() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('待辦清單');
const data = sheet.getDataRange().getValues();
const todos = [];
for (let i = 1; i < data.length; i++) {
if (data[i][4] === '待辦') {
todos.push({
id: data[i][0],
item: data[i][1],
created: data[i][2],
category: data[i][3]
});
}
}
return todos;
}
// ===== 獲取備忘錄列表 =====
function getMemoList() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('備忘錄');
const data = sheet.getDataRange().getValues();
const memos = [];
for (let i = 1; i < data.length; i++) {
memos.push({
id: data[i][0],
content: data[i][1],
created: data[i][2]
});
}
return memos.reverse().slice(0, 10);
}
// ===== 獲取今天的行程 =====
function getTodayEvents() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('行事曆');
const data = sheet.getDataRange().getValues();
const now = new Date();
const today = Utilities.formatDate(now, 'GMT+8', 'yyyy/MM/dd');
Logger.log('查詢今天的行程,今天是: ' + today);
const events = [];
for (let i = 1; i < data.length; i++) {
let eventDate = data[i][1];
let eventTime = data[i][2];
// ===== 處理日期 =====
if (typeof eventDate === 'string') {
try {
const dateObj = new Date(eventDate);
if (!isNaN(dateObj.getTime())) {
eventDate = Utilities.formatDate(dateObj, 'GMT+8', 'yyyy/MM/dd');
}
} catch (e) {
Logger.log('日期解析失敗: ' + eventDate);
}
} else if (eventDate instanceof Date) {
eventDate = Utilities.formatDate(eventDate, 'GMT+8', 'yyyy/MM/dd');
}
// ===== 處理時間(重要!)=====
if (eventTime instanceof Date) {
// 如果是 Date 物件,格式化為 HH:mm
eventTime = Utilities.formatDate(eventTime, 'GMT+8', 'HH:mm');
} else if (typeof eventTime === 'string') {
// 如果已經是字串,保持原樣
eventTime = eventTime;
} else {
// 其他情況,轉為字串
eventTime = String(eventTime);
}
Logger.log(`比對: Sheet中的日期=${eventDate}, 時間=${eventTime}, 今天=${today}, 狀態=${data[i][5]}`);
if (eventDate === today && data[i][5] === '待辦') {
events.push({
id: data[i][0],
date: eventDate,
time: eventTime,
item: data[i][3]
});
}
}
return events.sort((a, b) => a.time.localeCompare(b.time));
}
// ===== 獲取即將到來的行程 =====
function getUpcomingEvents(limit) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('行事曆');
const data = sheet.getDataRange().getValues();
const now = new Date();
const events = [];
for (let i = 1; i < data.length; i++) {
let eventDate = data[i][1];
let eventTime = data[i][2];
// 處理時間格式
if (eventTime instanceof Date) {
eventTime = Utilities.formatDate(eventTime, 'GMT+8', 'HH:mm');
} else if (typeof eventTime === 'string') {
eventTime = eventTime;
} else {
eventTime = String(eventTime);
}
// 處理日期
if (typeof eventDate === 'string') {
eventDate = eventDate;
} else if (eventDate instanceof Date) {
eventDate = Utilities.formatDate(eventDate, 'GMT+8', 'yyyy/MM/dd');
}
const eventDateTime = new Date(eventDate + ' ' + eventTime);
if (eventDateTime >= now && data[i][5] === '待辦') {
events.push({
date: eventDate,
time: eventTime,
item: data[i][3]
});
}
}
return events
.sort((a, b) => new Date(a.date + ' ' + a.time) - new Date(b.date + ' ' + b.time))
.slice(0, limit);
}
// ===== 查詢行程 =====
function querySchedule(params) {
return '請使用「今天」「待辦清單」「備忘錄」查詢';
}
// ===== 格式化待辦清單 =====
function formatTodoList(todos) {
if (todos.length === 0) {
return '📋 待辦清單是空的\n\n太棒了!沒有未完成的事項 🎉';
}
// 按類別分組
const grouped = {};
todos.forEach(todo => {
const category = todo.category || '其他';
if (!grouped[category]) grouped[category] = [];
grouped[category].push(todo);
});
let message = `📋 待辦清單(${todos.length}項)\n\n`;
const categoryEmoji = {
'工作': '💼', '購物': '🛒', '財務': '💰', '溝通': '📞',
'學習': '📚', '健康': '💪', '娛樂': '🎮', '家務': '🏠', '其他': '📝'
};
Object.keys(grouped).forEach(category => {
const emoji = categoryEmoji[category] || '📝';
message += `${emoji} 【${category}】\n`;
grouped[category].forEach(todo => {
message += ` #${todo.id} ${todo.item}\n`;
});
message += '\n';
});
message += `完成後輸入「完成#編號」`;
return message;
}
// ===== 格式化備忘錄列表 =====
function formatMemoList(memos) {
if (memos.length === 0) {
return '📝 備忘錄是空的\n\n還沒有任何備忘錄';
}
let message = `📝 備忘錄(最近${memos.length}筆)\n\n`;
memos.forEach(memo => {
message += `#${memo.id} ${memo.content}\n`;
message += ` ${memo.created}\n\n`;
});
return message;
}
// ===== 格式化今天行程 =====
function formatTodayEvents(events) {
const today = Utilities.formatDate(new Date(), 'GMT+8', 'yyyy/MM/dd (E)');
if (events.length === 0) {
return `📅 ${today}\n\n今天沒有安排行程\n\n好好享受自由時光!😊`;
}
let message = `📅 ${today}\n\n`;
events.forEach(event => {
message += `⏰ ${event.time} - ${event.item}\n`;
message += ` #${event.id}\n\n`;
});
message += `取消行程輸入「取消#編號」`;
return message;
}
// ===== 確保資料表存在 =====
function ensureDataExists() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
// 行事曆
let calendarSheet = ss.getSheetByName('行事曆');
if (!calendarSheet) {
calendarSheet = ss.insertSheet('行事曆');
calendarSheet.appendRow(['ID', '日期', '時間', '事項', '是否提醒', '狀態']);
}
// 待辦清單
let todoSheet = ss.getSheetByName('待辦清單');
if (!todoSheet) {
todoSheet = ss.insertSheet('待辦清單');
todoSheet.appendRow(['ID', '事項', '建立時間', '類別', '狀態', '完成時間']);
}
// 備忘錄
let memoSheet = ss.getSheetByName('備忘錄');
if (!memoSheet) {
memoSheet = ss.insertSheet('備忘錄');
memoSheet.appendRow(['ID', '內容', '建立時間']);
}
// 用戶資料
let userSheet = ss.getSheetByName('用戶資料');
if (!userSheet) {
userSheet = ss.insertSheet('用戶資料');
userSheet.appendRow(['User ID', '名稱', '最後活躍時間']);
}
}
// ===== 獲取明天的行程 =====
function getTomorrowEvents() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('行事曆');
const data = sheet.getDataRange().getValues();
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const tomorrowStr = Utilities.formatDate(tomorrow, 'GMT+8', 'yyyy/MM/dd');
return getEventsByDate(data, tomorrowStr);
}
// ===== 獲取後天的行程 =====
function getDayAfterTomorrowEvents() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('行事曆');
const data = sheet.getDataRange().getValues();
const dayAfter = new Date();
dayAfter.setDate(dayAfter.getDate() + 2);
const dayAfterStr = Utilities.formatDate(dayAfter, 'GMT+8', 'yyyy/MM/dd');
return getEventsByDate(data, dayAfterStr);
}
// ===== 獲取本週的行程 =====
function getThisWeekEvents() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('行事曆');
const data = sheet.getDataRange().getValues();
const now = new Date();
const weekStart = new Date(now);
weekStart.setDate(now.getDate() - now.getDay());
weekStart.setHours(0, 0, 0, 0);
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekStart.getDate() + 6);
weekEnd.setHours(23, 59, 59, 999);
return getEventsByDateRange(data, weekStart, weekEnd);
}
// ===== 獲取本月的行程 =====
function getThisMonthEvents() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('行事曆');
const data = sheet.getDataRange().getValues();
const now = new Date();
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999);
return getEventsByDateRange(data, monthStart, monthEnd);
}
// ===== 獲取所有行程(按月分組)=====
function getAllEvents() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('行事曆');
const data = sheet.getDataRange().getValues();
const grouped = {};
for (let i = 1; i < data.length; i++) {
if (data[i][5] !== '待辦') continue;
let eventDate = data[i][1];
let eventTime = data[i][2];
if (typeof eventDate === 'string') {
eventDate = eventDate;
} else if (eventDate instanceof Date) {
eventDate = Utilities.formatDate(eventDate, 'GMT+8', 'yyyy/MM/dd');
}
if (eventTime instanceof Date) {
eventTime = Utilities.formatDate(eventTime, 'GMT+8', 'HH:mm');
} else if (typeof eventTime === 'string') {
eventTime = eventTime;
} else {
eventTime = String(eventTime);
}
const monthKey = eventDate.substring(0, 7); // "2026/01"
if (!grouped[monthKey]) grouped[monthKey] = [];
grouped[monthKey].push({
id: data[i][0],
date: eventDate,
time: eventTime,
item: data[i][3]
});
}
// 排序
Object.keys(grouped).forEach(month => {
grouped[month].sort((a, b) => {
const dateTimeA = new Date(a.date + ' ' + a.time);
const dateTimeB = new Date(b.date + ' ' + b.time);
return dateTimeA - dateTimeB;
});
});
return grouped;
}
// ===== 獲取指定日期的行程 =====
function getSpecificDateEvents(dateStr) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('行事曆');
const data = sheet.getDataRange().getValues();
return getEventsByDate(data, dateStr);
}
// ===== 輔助函式:按日期獲取行程 =====
function getEventsByDate(data, targetDate) {
const events = [];
for (let i = 1; i < data.length; i++) {
let eventDate = data[i][1];
let eventTime = data[i][2];
if (typeof eventDate === 'string') {
eventDate = eventDate;
} else if (eventDate instanceof Date) {
eventDate = Utilities.formatDate(eventDate, 'GMT+8', 'yyyy/MM/dd');
}
if (eventTime instanceof Date) {
eventTime = Utilities.formatDate(eventTime, 'GMT+8', 'HH:mm');
} else if (typeof eventTime === 'string') {
eventTime = eventTime;
} else {
eventTime = String(eventTime);
}
if (eventDate === targetDate && data[i][5] === '待辦') {
events.push({
id: data[i][0],
date: eventDate,
time: eventTime,
item: data[i][3]
});
}
}
return events.sort((a, b) => a.time.localeCompare(b.time));
}
// ===== 輔助函式:按日期範圍獲取行程 =====
function getEventsByDateRange(data, startDate, endDate) {
const events = [];
for (let i = 1; i < data.length; i++) {
let eventDate = data[i][1];
let eventTime = data[i][2];
if (typeof eventDate === 'string') {
eventDate = eventDate;
} else if (eventDate instanceof Date) {
eventDate = Utilities.formatDate(eventDate, 'GMT+8', 'yyyy/MM/dd');
}
if (eventTime instanceof Date) {
eventTime = Utilities.formatDate(eventTime, 'GMT+8', 'HH:mm');
} else if (typeof eventTime === 'string') {
eventTime = eventTime;
} else {
eventTime = String(eventTime);
}
const eventDateTime = new Date(eventDate + ' ' + eventTime);
if (eventDateTime >= startDate && eventDateTime <= endDate && data[i][5] === '待辦') {
events.push({
id: data[i][0],
date: eventDate,
time: eventTime,
item: data[i][3]
});
}
}
return events.sort((a, b) => {
const dateTimeA = new Date(a.date + ' ' + a.time);
const dateTimeB = new Date(b.date + ' ' + b.time);
return dateTimeA - dateTimeB;
});
}
// ===== 批量新增行程 =====
function addMultipleEvents(eventsArray) {
if (!eventsArray || eventsArray.length === 0) {
return { success: false, events: [] };
}
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('行事曆');
const results = [];
eventsArray.forEach(eventData => {
const lastRow = sheet.getLastRow();
const newId = lastRow > 1 ? sheet.getRange(lastRow, 1).getValue() + 1 : 1;
const date = eventData.date || '';
const time = eventData.time || '';
const item = eventData.item || '';
sheet.appendRow([
newId,
date,
time,
item,
'未提醒',
'待辦'
]);
results.push({
id: newId,
date: date,
time: time,
item: item
});
});
return { success: true, events: results };
}
// ===== 刪除待辦 =====
function deleteTodo(id) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('待辦清單');
const data = sheet.getDataRange().getValues();
for (let i = 1; i < data.length; i++) {
if (data[i][0] == id) {
const item = data[i][1];
sheet.deleteRow(i + 1);
return {
success: true,
message: `✅ 已刪除待辦\n\n📝 ${item}`
};
}
}
return {
success: false,
message: `❌ 找不到編號 #${id} 的待辦事項`
};
}
// ===== 刪除備忘錄 =====
function deleteMemo(id) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('備忘錄');
const data = sheet.getDataRange().getValues();
for (let i = 1; i < data.length; i++) {
if (data[i][0] == id) {
const content = data[i][1];
sheet.deleteRow(i + 1);
return {
success: true,
message: `✅ 已刪除備忘錄\n\n📝 ${content}`
};
}
}
return {
success: false,
message: `❌ 找不到編號 #${id} 的備忘錄`
};
}
// ===== 完成行程 =====
function completeEvent(id) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('行事曆');
const data = sheet.getDataRange().getValues();
for (let i = 1; i < data.length; i++) {
if (data[i][0] == id) {
const item = data[i][3];
// 將狀態改為「已完成」
sheet.getRange(i + 1, 6).setValue('已完成');
return {
success: true,
message: `🎉 太棒了!\n\n✅ 已完成行程:${item}`
};
}
}
return {
success: false,
message: `❌ 找不到編號 #${id} 的行程`
};
}
4. LineAPI.gs (訊息回覆)
處理 Messaging API 的回覆功能。
// ===== 回覆訊息 =====
function replyToLine(replyToken, message) {
const accessToken = PropertiesService.getScriptProperties().getProperty('LINE_CHANNEL_ACCESS_TOKEN');
const url = 'https://api.line.me/v2/bot/message/reply';
const payload = {
replyToken: replyToken,
messages: [{
type: 'text',
text: message
}]
};
const options = {
method: 'post',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + accessToken
},
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
try {
UrlFetchApp.fetch(url, options);
} catch (error) {
Logger.log('Reply Error: ' + error);
}
}
// ===== 回覆行程記錄成功卡片 =====
function replyEventSuccessCard(replyToken, result) {
const accessToken = PropertiesService.getScriptProperties().getProperty('LINE_CHANNEL_ACCESS_TOKEN');
const url = 'https://api.line.me/v2/bot/message/reply';
const flexMessage = {
type: "bubble",
hero: {
type: "box",
layout: "vertical",
contents: [
{
type: "text",
text: "✅ 已記錄行程",
weight: "bold",
size: "xl",
color: "#FFFFFF"
}
],
backgroundColor: "#4CAF50",
paddingAll: "20px"
},
body: {
type: "box",
layout: "vertical",
contents: [
{
type: "box",
layout: "baseline",
contents: [
{
type: "text",
text: "📅",
size: "lg",
flex: 0
},
{
type: "text",
text: "日期時間",
size: "sm",
color: "#999999",
flex: 2,
margin: "md"
},
{
type: "text",
text: `${result.date} ${result.time}`,
size: "sm",
color: "#111111",
flex: 4,
wrap: true
}
],
spacing: "sm"
},
{
type: "separator",
margin: "lg"
},
{
type: "box",
layout: "baseline",
contents: [
{
type: "text",
text: "📝",
size: "lg",
flex: 0
},
{
type: "text",
text: "事項",
size: "sm",
color: "#999999",
flex: 2,
margin: "md"
},
{
type: "text",
text: result.item,
size: "sm",
color: "#111111",
flex: 4,
wrap: true
}
],
spacing: "sm",
margin: "lg"
},
{
type: "separator",
margin: "lg"
},
{
type: "box",
layout: "baseline",
contents: [
{
type: "text",
text: "🔢",
size: "lg",
flex: 0
},
{
type: "text",
text: "編號",
size: "sm",
color: "#999999",
flex: 2,
margin: "md"
},
{
type: "text",
text: `#${result.id}`,
size: "sm",
color: "#FF6B6B",
flex: 4,
weight: "bold"
}
],
spacing: "sm",
margin: "lg"
},
{
type: "separator",
margin: "lg"
},
{
type: "box",
layout: "baseline",
contents: [
{
type: "text",
text: "⏰",
size: "lg",
flex: 0
},
{
type: "text",
text: "提醒",
size: "sm",
color: "#999999",
flex: 2,
margin: "md"
},
{
type: "text",
text: "會在30分鐘前提醒你",
size: "sm",
color: "#4CAF50",
flex: 4,
wrap: true
}
],
spacing: "sm",
margin: "lg"
}
]
}
};
const payload = {
replyToken: replyToken,
messages: [{
type: 'flex',
altText: `已記錄行程:${result.item}`,
contents: flexMessage
}]
};
const options = {
method: 'post',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + accessToken
},
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
UrlFetchApp.fetch(url, options);
}
// ===== 回覆待辦記錄成功卡片 =====
function replyTodoSuccessCard(replyToken, result) {
const accessToken = PropertiesService.getScriptProperties().getProperty('LINE_CHANNEL_ACCESS_TOKEN');
const url = 'https://api.line.me/v2/bot/message/reply';
const categoryEmoji = {
'工作': '💼', '購物': '🛒', '財務': '💰', '溝通': '📞',
'學習': '📚', '健康': '💪', '娛樂': '🎮', '家務': '🏠', '其他': '📝'
};
const emoji = categoryEmoji[result.category] || '📝';
const flexMessage = {
type: "bubble",
hero: {
type: "box",
layout: "vertical",
contents: [
{
type: "text",
text: "✅ 已加入待辦清單",
weight: "bold",
size: "xl",
color: "#FFFFFF"
}
],
backgroundColor: "#2196F3",
paddingAll: "20px"
},
body: {
type: "box",
layout: "vertical",
contents: [
{
type: "box",
layout: "baseline",
contents: [
{
type: "text",
text: emoji,
size: "lg",
flex: 0
},
{
type: "text",
text: "事項",
size: "sm",
color: "#999999",
flex: 2,
margin: "md"
},
{
type: "text",
text: result.item,
size: "sm",
color: "#111111",
flex: 4,
wrap: true
}
],
spacing: "sm"
},
{
type: "separator",
margin: "lg"
},
{
type: "box",
layout: "baseline",
contents: [
{
type: "text",
text: "📂",
size: "lg",
flex: 0
},
{
type: "text",
text: "類別",
size: "sm",
color: "#999999",
flex: 2,
margin: "md"
},
{
type: "text",
text: result.category,
size: "sm",
color: "#2196F3",
flex: 4,
weight: "bold"
}
],
spacing: "sm",
margin: "lg"
},
{
type: "separator",
margin: "lg"
},
{
type: "box",
layout: "baseline",
contents: [
{
type: "text",
text: "🔢",
size: "lg",
flex: 0
},
{
type: "text",
text: "編號",
size: "sm",
color: "#999999",
flex: 2,
margin: "md"
},
{
type: "text",
text: `#${result.id}`,
size: "sm",
color: "#FF6B6B",
flex: 4,
weight: "bold"
}
],
spacing: "sm",
margin: "lg"
}
]
},
footer: {
type: "box",
layout: "vertical",
contents: [
{
type: "text",
text: `完成後請輸入「完成#${result.id}」`,
size: "xs",
color: "#999999",
align: "center"
}
],
paddingAll: "12px"
}
};
const payload = {
replyToken: replyToken,
messages: [{
type: 'flex',
altText: `已加入待辦:${result.item}`,
contents: flexMessage
}]
};
const options = {
method: 'post',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + accessToken
},
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
UrlFetchApp.fetch(url, options);
}
// ===== 回覆備忘錄記錄成功卡片 =====
function replyMemoSuccessCard(replyToken, result) {
const accessToken = PropertiesService.getScriptProperties().getProperty('LINE_CHANNEL_ACCESS_TOKEN');
const url = 'https://api.line.me/v2/bot/message/reply';
const flexMessage = {
type: "bubble",
hero: {
type: "box",
layout: "vertical",
contents: [
{
type: "text",
text: "✅ 已儲存備忘錄",
weight: "bold",
size: "xl",
color: "#FFFFFF"
}
],
backgroundColor: "#9C27B0",
paddingAll: "20px"
},
body: {
type: "box",
layout: "vertical",
contents: [
{
type: "box",
layout: "baseline",
contents: [
{
type: "text",
text: "📝",
size: "lg",
flex: 0
},
{
type: "text",
text: "內容",
size: "sm",
color: "#999999",
flex: 2,
margin: "md"
},
{
type: "text",
text: result.content,
size: "sm",
color: "#111111",
flex: 4,
wrap: true
}
],
spacing: "sm"
},
{
type: "separator",
margin: "lg"
},
{
type: "box",
layout: "baseline",
contents: [
{
type: "text",
text: "🔢",
size: "lg",
flex: 0
},
{
type: "text",
text: "編號",
size: "sm",
color: "#999999",
flex: 2,
margin: "md"
},
{
type: "text",
text: `#${result.id}`,
size: "sm",
color: "#FF6B6B",
flex: 4,
weight: "bold"
}
],
spacing: "sm",
margin: "lg"
}
]
}
};
const payload = {
replyToken: replyToken,
messages: [{
type: 'flex',
altText: `已儲存備忘錄`,
contents: flexMessage
}]
};
const options = {
method: 'post',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + accessToken
},
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
UrlFetchApp.fetch(url, options);
}
// ===== 回覆今日行程卡片(加入按鈕)=====
function replyTodayEventsCard(replyToken, events) {
const today = Utilities.formatDate(new Date(), 'GMT+8', 'MM/dd (E)');
replyDayEventsCardWithButtons(replyToken, events, `今天 ${today}`, "#FF6B6B");
}
// ===== 回覆指定日期行程卡片(加入按鈕)=====
function replyDayEventsCard(replyToken, events, dateLabel) {
replyDayEventsCardWithButtons(replyToken, events, dateLabel, "#FF9800");
}
// ===== 通用單日行程卡片(按鈕在下方)=====
function replyDayEventsCardWithButtons(replyToken, events, title, color) {
const accessToken = PropertiesService.getScriptProperties().getProperty('LINE_CHANNEL_ACCESS_TOKEN');
const url = 'https://api.line.me/v2/bot/message/reply';
if (events.length === 0) {
replyToLine(replyToken, `📅 ${title}\n\n這天沒有安排行程\n\n好好享受自由時光!😊`);
return;
}
// 建立事項列表(按鈕在下方)
const eventBoxes = events.map(event => {
return {
type: "box",
layout: "vertical",
contents: [
{
type: "box",
layout: "horizontal",
contents: [
{
type: "text",
text: `#${event.id}`,
size: "xs",
color: "#999999",
flex: 0
},
{
type: "text",
text: event.time,
size: "sm",
weight: "bold",
color: color,
flex: 0,
margin: "md"
},
{
type: "text",
text: event.item,
size: "sm",
wrap: true,
color: "#111111",
flex: 1,
margin: "md"
}
]
},
{
type: "box",
layout: "horizontal",
contents: [
{
type: "button",
action: {
type: "message",
label: "✅ 完成",
text: `完成行程#${event.id}`
},
style: "primary",
color: "#4CAF50",
height: "sm"
},
{
type: "button",
action: {
type: "message",
label: "❌ 刪除",
text: `刪除行程#${event.id}`
},
style: "secondary",
height: "sm",
margin: "xs"
}
],
margin: "sm"
}
],
margin: "md",
paddingAll: "md",
backgroundColor: "#F5F5F5",
cornerRadius: "md"
};
});
const flexMessage = {
type: "bubble",
hero: {
type: "box",
layout: "vertical",
contents: [
{
type: "text",
text: `📅 ${title}`,
weight: "bold",
size: "xl",
color: "#FFFFFF"
}
],
backgroundColor: color,
paddingAll: "20px"
},
body: {
type: "box",
layout: "vertical",
contents: [
{
type: "box",
layout: "horizontal",
contents: [
{
type: "text",
text: "🔥",
size: "xxl",
flex: 0
},
{
type: "box",
layout: "vertical",
contents: [
{
type: "text",
text: `${events.length}`,
weight: "bold",
size: "xxl",
color: color
},
{
type: "text",
text: "個行程",
size: "sm",
color: "#999999"
}
],
margin: "md"
}
],
margin: "lg"
},
{
type: "separator",
margin: "xl"
},
{
type: "box",
layout: "vertical",
contents: eventBoxes,
margin: "lg"
}
]
}
};
const payload = {
replyToken: replyToken,
messages: [{
type: 'flex',
altText: `${title}有 ${events.length} 個行程`,
contents: flexMessage
}]
};
const options = {
method: 'post',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + accessToken
},
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
UrlFetchApp.fetch(url, options);
}
// ===== 回覆本週行程卡片(簡化版,不加按鈕)=====
function replyWeekEventsCard(replyToken, events) {
const accessToken = PropertiesService.getScriptProperties().getProperty('LINE_CHANNEL_ACCESS_TOKEN');
const url = 'https://api.line.me/v2/bot/message/reply';
if (events.length === 0) {
replyToLine(replyToken, `📅 本週行程\n\n本週沒有安排行程\n\n好好享受自由時光!😊`);
return;
}
// 按日期分組
const grouped = {};
events.forEach(event => {
const dateKey = event.date;
if (!grouped[dateKey]) grouped[dateKey] = [];
grouped[dateKey].push(event);
});
// 建立每日行程
const dayBoxes = [];
Object.keys(grouped).sort().forEach(date => {
const dayEvents = grouped[date];
const dateObj = new Date(date);
const dayLabel = Utilities.formatDate(dateObj, 'GMT+8', 'MM/dd (E)');
dayBoxes.push({
type: "box",
layout: "vertical",
contents: [
{
type: "text",
text: dayLabel,
size: "sm",
weight: "bold",
color: "#4CAF50"
}
],
margin: "lg"
});
dayEvents.forEach(event => {
dayBoxes.push({
type: "box",
layout: "horizontal",
contents: [
{
type: "text",
text: `#${event.id}`,
size: "xs",
color: "#999999",
flex: 1
},
{
type: "text",
text: event.time,
size: "sm",
color: "#666666",
flex: 2,
margin: "md"
},
{
type: "text",
text: event.item,
size: "sm",
wrap: true,
color: "#111111",
flex: 5,
margin: "md"
}
],
margin: "sm",
spacing: "sm"
});
});
});
const flexMessage = {
type: "bubble",
hero: {
type: "box",
layout: "vertical",
contents: [
{
type: "text",
text: "📅 本週行程",
weight: "bold",
size: "xl",
color: "#FFFFFF"
}
],
backgroundColor: "#4CAF50",
paddingAll: "20px"
},
body: {
type: "box",
layout: "vertical",
contents: [
{
type: "box",
layout: "horizontal",
contents: [
{
type: "text",
text: "📆",
size: "xxl",
flex: 0
},
{
type: "box",
layout: "vertical",
contents: [
{
type: "text",
text: `${events.length}`,
weight: "bold",
size: "xxl",
color: "#4CAF50"
},
{
type: "text",
text: "個行程",
size: "sm",
color: "#999999"
}
],
margin: "md"
}
],
margin: "lg"
},
{
type: "separator",
margin: "xl"
},
{
type: "box",
layout: "vertical",
contents: dayBoxes,
margin: "lg"
}
]
},
footer: {
type: "box",
layout: "vertical",
contents: [
{
type: "text",
text: "輸入指定日期查看詳細(例如:今天、明天)",
size: "xs",
color: "#999999",
align: "center",
wrap: true
}
],
paddingAll: "12px"
}
};
const payload = {
replyToken: replyToken,
messages: [{
type: 'flex',
altText: `本週有 ${events.length} 個行程`,
contents: flexMessage
}]
};
const options = {
method: 'post',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + accessToken
},
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
UrlFetchApp.fetch(url, options);
}
// ===== 回覆本月行程卡片 =====
function replyMonthEventsCard(replyToken, events) {
const accessToken = PropertiesService.getScriptProperties().getProperty('LINE_CHANNEL_ACCESS_TOKEN');
const url = 'https://api.line.me/v2/bot/message/reply';
const now = new Date();
const monthLabel = Utilities.formatDate(now, 'GMT+8', 'yyyy年MM月');
if (events.length === 0) {
replyToLine(replyToken, `📅 ${monthLabel}\n\n本月沒有安排行程\n\n好好享受自由時光!😊`);
return;
}
// 只顯示前20個(避免卡片太長)
const displayEvents = events.slice(0, 20);
const eventBoxes = displayEvents.map(event => {
return {
type: "box",
layout: "horizontal",
contents: [
{
type: "text",
text: `#${event.id}`,
size: "xs",
color: "#999999",
flex: 1
},
{
type: "text",
text: `${event.date} ${event.time}`,
size: "xs",
color: "#666666",
flex: 4,
margin: "md"
},
{
type: "text",
text: event.item,
size: "sm",
wrap: true,
color: "#111111",
flex: 4,
margin: "md"
}
],
margin: "sm",
spacing: "sm"
};
});
const flexMessage = {
type: "bubble",
hero: {
type: "box",
layout: "vertical",
contents: [
{
type: "text",
text: `📅 ${monthLabel}`,
weight: "bold",
size: "xl",
color: "#FFFFFF"
}
],
backgroundColor: "#2196F3",
paddingAll: "20px"
},
body: {
type: "box",
layout: "vertical",
contents: [
{
type: "box",
layout: "horizontal",
contents: [
{
type: "text",
text: "📆",
size: "xxl",
flex: 0
},
{
type: "box",
layout: "vertical",
contents: [
{
type: "text",
text: `${events.length}`,
weight: "bold",
size: "xxl",
color: "#2196F3"
},
{
type: "text",
text: "個行程",
size: "sm",
color: "#999999"
}
],
margin: "md"
}
],
margin: "lg"
},
{
type: "separator",
margin: "xl"
},
{
type: "box",
layout: "vertical",
contents: eventBoxes,
margin: "lg"
}
]
},
footer: events.length > 20 ? {
type: "box",
layout: "vertical",
contents: [
{
type: "text",
text: `僅顯示前20個,共${events.length}個行程`,
size: "xs",
color: "#999999",
align: "center"
}
],
paddingAll: "12px"
} : undefined
};
const payload = {
replyToken: replyToken,
messages: [{
type: 'flex',
altText: `${monthLabel}有 ${events.length} 個行程`,
contents: flexMessage
}]
};
const options = {
method: 'post',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + accessToken
},
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
UrlFetchApp.fetch(url, options);
}
// ===== 回覆全部行程卡片(按月分組)=====
function replyAllEventsCard(replyToken, groupedEvents) {
const accessToken = PropertiesService.getScriptProperties().getProperty('LINE_CHANNEL_ACCESS_TOKEN');
const url = 'https://api.line.me/v2/bot/message/reply';
const months = Object.keys(groupedEvents).sort();
if (months.length === 0) {
replyToLine(replyToken, `📅 所有行程\n\n目前沒有任何行程\n\n開始規劃你的生活吧!😊`);
return;
}
// 計算總數
let totalCount = 0;
months.forEach(month => {
totalCount += groupedEvents[month].length;
});
// 建立月份小卡
const monthBoxes = months.map(month => {
const events = groupedEvents[month];
const monthLabel = month.replace('/', '年') + '月';
return {
type: "box",
layout: "horizontal",
contents: [
{
type: "box",
layout: "vertical",
contents: [
{
type: "text",
text: monthLabel,
size: "sm",
weight: "bold",
color: "#FFFFFF"
},
{
type: "text",
text: `${events.length} 個行程`,
size: "xs",
color: "#FFFFFF",
margin: "sm"
}
],
backgroundColor: "#9C27B0",
cornerRadius: "md",
paddingAll: "md",
flex: 1
}
],
margin: "md"
};
});
const flexMessage = {
type: "bubble",
hero: {
type: "box",
layout: "vertical",
contents: [
{
type: "text",
text: "📅 所有行程",
weight: "bold",
size: "xl",
color: "#FFFFFF"
}
],
backgroundColor: "#9C27B0",
paddingAll: "20px"
},
body: {
type: "box",
layout: "vertical",
contents: [
{
type: "box",
layout: "horizontal",
contents: [
{
type: "text",
text: "📊",
size: "xxl",
flex: 0
},
{
type: "box",
layout: "vertical",
contents: [
{
type: "text",
text: `${totalCount}`,
weight: "bold",
size: "xxl",
color: "#9C27B0"
},
{
type: "text",
text: "個行程",
size: "sm",
color: "#999999"
}
],
margin: "md"
}
],
margin: "lg"
},
{
type: "separator",
margin: "xl"
},
{
type: "text",
text: "各月分布",
size: "sm",
color: "#999999",
margin: "lg"
},
{
type: "box",
layout: "vertical",
contents: monthBoxes,
margin: "md"
}
]
},
footer: {
type: "box",
layout: "vertical",
contents: [
{
type: "text",
text: "輸入「本週」或「本月」查看詳細行程",
size: "xs",
color: "#999999",
align: "center"
}
],
paddingAll: "12px"
}
};
const payload = {
replyToken: replyToken,
messages: [{
type: 'flex',
altText: `共有 ${totalCount} 個行程`,
contents: flexMessage
}]
};
const options = {
method: 'post',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + accessToken
},
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
UrlFetchApp.fetch(url, options);
}
// ===== 回覆待辦清單卡片(按鈕在下方)=====
function replyTodoListCard(replyToken, todos) {
const accessToken = PropertiesService.getScriptProperties().getProperty('LINE_CHANNEL_ACCESS_TOKEN');
const url = 'https://api.line.me/v2/bot/message/reply';
if (todos.length === 0) {
replyToLine(replyToken, '📋 待辦清單是空的\n\n太棒了!沒有未完成的事項 🎉');
return;
}
const categoryEmoji = {
'工作': '💼', '購物': '🛒', '財務': '💰', '溝通': '📞',
'學習': '📚', '健康': '💪', '娛樂': '🎮', '家務': '🏠', '其他': '📝'
};
// 按類別分組
const grouped = {};
todos.forEach(todo => {
const category = todo.category || '其他';
if (!grouped[category]) grouped[category] = [];
grouped[category].push(todo);
});
// 建立待辦列表(按鈕在下方)
const todoBoxes = [];
Object.keys(grouped).forEach(category => {
const emoji = categoryEmoji[category] || '📝';
todoBoxes.push({
type: "box",
layout: "horizontal",
contents: [
{
type: "text",
text: `${emoji} ${category}`,
size: "sm",
weight: "bold",
color: "#666666"
}
],
margin: "lg"
});
grouped[category].forEach(todo => {
todoBoxes.push({
type: "box",
layout: "vertical",
contents: [
{
type: "box",
layout: "horizontal",
contents: [
{
type: "text",
text: `#${todo.id}`,
size: "xs",
color: "#999999",
flex: 0
},
{
type: "text",
text: todo.item,
size: "sm",
wrap: true,
color: "#111111",
flex: 1,
margin: "md"
}
]
},
{
type: "box",
layout: "horizontal",
contents: [
{
type: "button",
action: {
type: "message",
label: "✅ 完成",
text: `完成#${todo.id}`
},
style: "primary",
color: "#4CAF50",
height: "sm"
},
{
type: "button",
action: {
type: "message",
label: "❌ 刪除",
text: `刪除待辦#${todo.id}`
},
style: "secondary",
height: "sm",
margin: "xs"
}
],
margin: "sm"
}
],
margin: "sm",
paddingAll: "md",
backgroundColor: "#F5F5F5",
cornerRadius: "md"
});
});
});
const flexMessage = {
type: "bubble",
hero: {
type: "box",
layout: "vertical",
contents: [
{
type: "text",
text: "📋 待辦清單",
weight: "bold",
size: "xl",
color: "#FFFFFF"
}
],
backgroundColor: "#4CAF50",
paddingAll: "20px"
},
body: {
type: "box",
layout: "vertical",
contents: [
{
type: "box",
layout: "horizontal",
contents: [
{
type: "text",
text: "✅",
size: "xxl",
flex: 0
},
{
type: "box",
layout: "vertical",
contents: [
{
type: "text",
text: `${todos.length}`,
weight: "bold",
size: "xxl",
color: "#4CAF50"
},
{
type: "text",
text: "個待辦",
size: "sm",
color: "#999999"
}
],
margin: "md"
}
],
margin: "lg"
},
{
type: "separator",
margin: "xl"
},
{
type: "box",
layout: "vertical",
contents: todoBoxes,
margin: "lg"
}
]
}
};
const payload = {
replyToken: replyToken,
messages: [{
type: 'flex',
altText: `待辦清單(${todos.length}項)`,
contents: flexMessage
}]
};
const options = {
method: 'post',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + accessToken
},
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
UrlFetchApp.fetch(url, options);
}
// ===== 回覆備忘錄卡片(修正按鈕)=====
function replyMemoListCard(replyToken, memos) {
const accessToken = PropertiesService.getScriptProperties().getProperty('LINE_CHANNEL_ACCESS_TOKEN');
const url = 'https://api.line.me/v2/bot/message/reply';
if (memos.length === 0) {
replyToLine(replyToken, '📝 備忘錄是空的\n\n還沒有任何備忘錄');
return;
}
const memoBoxes = memos.map(memo => {
const dateStr = typeof memo.created === 'string'
? memo.created
: Utilities.formatDate(new Date(memo.created), 'GMT+8', 'MM/dd HH:mm');
return {
type: "box",
layout: "vertical",
contents: [
{
type: "box",
layout: "horizontal",
contents: [
{
type: "text",
text: `#${memo.id}`,
size: "xs",
color: "#999999",
flex: 1
},
{
type: "text",
text: dateStr,
size: "xs",
color: "#999999",
flex: 3,
margin: "md"
},
{
type: "button",
action: {
type: "message",
label: "❌",
text: `刪除備忘#${memo.id}`
},
style: "secondary",
height: "sm",
flex: 2
}
],
margin: "md"
},
{
type: "text",
text: memo.content,
size: "sm",
wrap: true,
color: "#111111",
margin: "sm"
},
{
type: "separator",
margin: "md"
}
]
};
});
const flexMessage = {
type: "bubble",
hero: {
type: "box",
layout: "vertical",
contents: [
{
type: "text",
text: "📝 備忘錄",
weight: "bold",
size: "xl",
color: "#FFFFFF"
}
],
backgroundColor: "#2196F3",
paddingAll: "20px"
},
body: {
type: "box",
layout: "vertical",
contents: [
{
type: "box",
layout: "horizontal",
contents: [
{
type: "text",
text: "📌",
size: "xxl",
flex: 0
},
{
type: "box",
layout: "vertical",
contents: [
{
type: "text",
text: `${memos.length}`,
weight: "bold",
size: "xxl",
color: "#2196F3"
},
{
type: "text",
text: "筆備忘",
size: "sm",
color: "#999999"
}
],
margin: "md"
}
],
margin: "lg"
},
{
type: "separator",
margin: "xl"
},
{
type: "box",
layout: "vertical",
contents: memoBoxes,
margin: "lg"
}
]
},
footer: {
type: "box",
layout: "vertical",
contents: [
{
type: "text",
text: "❌ 刪除",
size: "xs",
color: "#999999",
align: "center"
}
],
paddingAll: "12px"
}
};
const payload = {
replyToken: replyToken,
messages: [{
type: 'flex',
altText: `備忘錄(${memos.length}筆)`,
contents: flexMessage
}]
};
const options = {
method: 'post',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + accessToken
},
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
UrlFetchApp.fetch(url, options);
}
// ===== Push 訊息(提醒用)=====
function pushToLine(userId, message) {
const accessToken = PropertiesService.getScriptProperties().getProperty('LINE_CHANNEL_ACCESS_TOKEN');
const url = 'https://api.line.me/v2/bot/message/push';
const payload = {
to: userId,
messages: [{
type: 'text',
text: message
}]
};
const options = {
method: 'post',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + accessToken
},
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
try {
UrlFetchApp.fetch(url, options);
} catch (error) {
Logger.log('Push Error: ' + error);
}
}
// ===== 回覆批量行程記錄成功卡片 =====
function replyMultipleEventsSuccessCard(replyToken, results) {
const accessToken = PropertiesService.getScriptProperties().getProperty('LINE_CHANNEL_ACCESS_TOKEN');
const url = 'https://api.line.me/v2/bot/message/reply';
if (!results.success || results.events.length === 0) {
replyToLine(replyToken, '❌ 行程記錄失敗,請再試一次');
return;
}
// 建立行程列表
const eventBoxes = results.events.map(event => {
return {
type: "box",
layout: "horizontal",
contents: [
{
type: "text",
text: `#${event.id}`,
size: "xs",
color: "#999999",
flex: 1
},
{
type: "text",
text: event.time,
size: "sm",
weight: "bold",
color: "#FF6B6B",
flex: 2,
margin: "md"
},
{
type: "text",
text: event.item,
size: "sm",
wrap: true,
color: "#111111",
flex: 4,
margin: "md"
}
],
margin: "md",
spacing: "sm"
};
});
const flexMessage = {
type: "bubble",
hero: {
type: "box",
layout: "vertical",
contents: [
{
type: "text",
text: "✅ 已記錄多個行程",
weight: "bold",
size: "xl",
color: "#FFFFFF"
}
],
backgroundColor: "#4CAF50",
paddingAll: "20px"
},
body: {
type: "box",
layout: "vertical",
contents: [
{
type: "box",
layout: "horizontal",
contents: [
{
type: "text",
text: "🔥",
size: "xxl",
flex: 0
},
{
type: "box",
layout: "vertical",
contents: [
{
type: "text",
text: `${results.events.length}`,
weight: "bold",
size: "xxl",
color: "#4CAF50"
},
{
type: "text",
text: "個行程",
size: "sm",
color: "#999999"
}
],
margin: "md"
}
],
margin: "lg"
},
{
type: "separator",
margin: "xl"
},
{
type: "box",
layout: "vertical",
contents: eventBoxes,
margin: "lg"
},
{
type: "separator",
margin: "xl"
},
{
type: "box",
layout: "baseline",
contents: [
{
type: "text",
text: "⏰",
size: "lg",
flex: 0
},
{
type: "text",
text: "提醒",
size: "sm",
color: "#999999",
flex: 2,
margin: "md"
},
{
type: "text",
text: "會在30分鐘前提醒你",
size: "sm",
color: "#4CAF50",
flex: 4,
wrap: true
}
],
spacing: "sm",
margin: "lg"
}
]
}
};
const payload = {
replyToken: replyToken,
messages: [{
type: 'flex',
altText: `已記錄 ${results.events.length} 個行程`,
contents: flexMessage
}]
};
const options = {
method: 'post',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + accessToken
},
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
UrlFetchApp.fetch(url, options);
}
部署與環境變數設定
程式碼撰寫完成後,必須設定環境變數並部署為 Web App,才能讓 LINE 伺服器正確呼叫您的 Apps Script。
- 設定環境變數:
- 在 Apps Script 左側點擊「專案設定」(齒輪圖示)。
- 新增以下「指令碼屬性」
- LINE_CHANNEL_ACCESS_TOKEN: (從 Step 1 取得)
- GEMINI_API_KEY: (從 Step 2 取得)
- 部署 Web App:
- 點擊右上角「部署」→「新增部署作業」。
- 類型選擇「網路應用程式」。
- 具有存取權的使用者:務必選擇「所有人」(否則 LINE 無法呼叫)。
- 點擊部署,複製生成的 Web App URL。
- 設定 LINE Webhook:
- 回到 LINE Developers Console 的 Messaging API 頁面。
- 在 Webhook URL 欄位貼上剛剛的網址。
- 開啟「Use webhook」。
- 點擊「Verify」,出現 Success 即代表成功!
AI 助理完整指令列表與實測
這個 AI 助理 支援自然語言指令,您可以直接輸入口語化的句子,Gemini AI 會自動解析並執行對應動作。
我們在實驗室針對不同情境進行了壓力測試,以下是支援的指令清單:
📅 行程管理
- 新增:「明天下午 3 點開會」、「1/20 早上 9 點面試」。
- 查詢:「今天的行程」、「下週有什麼事」。
- AI 智能:輸入「週五晚上要吃飯」,AI 會自動抓取本週五的日期與晚上的時間(如 19:00)。
✅ 待辦清單
- 新增:「記得要買牛奶」、「提醒我繳電話費」。
- AI 分類:系統會自動將「買牛奶」歸類為 [購物],「繳費」歸類為 [財務]。
- 查詢:「待辦清單」、「還有什麼沒做」。
📝 備忘錄
- 指令:「記一下會議室密碼是 1234」、「備註客戶喜歡喝拿鐵」。
延伸應用:從個人 Bot 到企業級系統開發
當您的需求從「個人助理」擴展到「企業應用」時,單純的 LINE Bot 開發 可能會遇到瓶頸。
1. Google Apps Script 的極限
雖然 GAS 免費且強大,但它有執行時間限制 (6 分鐘) 與配額限制。如果您的 AI 助理 需要服務數千名員工或客戶,回應速度可能會變慢。
2. AppSheet:無程式碼開發的下一步
如果您需要更複雜的資料管理、權限控管或手機 App 介面,我們建議導入 AppSheet 服務。它可以直接讀取同一個 Google Sheet,快速生成專業的企業 App,並能與 LINE Bot 連動。
3. 專業系統開發與 App 設計
對於需要高併發、複雜商業邏輯(如電商、金流、即時庫存)的需求,克隆資訊實驗室建議採用客製化的 系統開發 與 App 設計。透過將後端遷移至 Google Cloud Platform (GCP) 或 AWS,結合 React Native 或 Flutter 開發原生 App,能提供最極致的用戶體驗。
💡 常見問題
Q1: LINE Bot 開發使用 GAS 真的完全免費嗎?
A: 是的,對於個人開發者與中小型應用,Google Apps Script 與 Gemini API (免費層級) 的額度非常充足。除非您的機器人每天有數千次對話,否則基本不需要付費。
Q2: AI 助理回應顯示「系統繁忙」怎麼辦?
A: 這通常是因為 API Key 設定錯誤或 Apps Script 執行逾時。請檢查「專案設定」中的 GEMINI_API_KEY 是否正確,並到 Apps Script 的「執行項目」查看詳細錯誤日誌。
Q3: 我可以將此 Bot 串接到 AppSheet 服務嗎?
A: 可以!由於兩者都使用 Google Sheets 作為資料庫,您可以利用 AppSheet 建立管理後台,讓管理員透過 App 新增行程,而用戶透過 LINE Bot 接收通知,達成完美的系統整合。
👤 關於作者
本文由克隆資訊實驗室撰寫,團隊專精於 LINE Bot 開發、AppSheet 服務 與客製化 系統開發。我們致力於將最新的 AI 技術轉化為實用的商業解決方案。所有教學程式碼均經過實驗室實際部署驗證。
