2026 終極實戰!LINE Bot 開發教學:0 元打造 Gemini AI 助理 (附原始碼)

LINE Bot 開發架構圖,展示 Messaging API 與 Google Apps Script 和 Gemini API 的連接

目錄

☁️ 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 APIGoogle 最新 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 (用戶輸入自然語言,如「明天下午三點開會」)。

  1. 橋樑:Messaging API 接收訊息並轉發。
  2. 核心:Google Apps Script 接收 Webhook,進行邏輯處理。
  3. 腦袋:Gemini API 解析語意,判斷是「行程」、「待辦」還是「閒聊」。
  4. 記憶:Google Sheets 儲存資料。

LINE Bot 開發事前準備:帳號與環境設定

開始開發前,您需要準備 LINE Developers 帳號、Google Cloud 專案以取得 Gemini API Key,以及一個 Google 帳號用於存取 Apps Script。

Step 1: 取得 LINE Channel (Messaging API)

這是 LINE Bot 開發 的第一步,我們需要一個「身分證」讓程式與 LINE 溝通。

  1. 註冊 LINE Developers:前往 LINE Developers,使用您的 LINE 帳號登入。
  2. 建立 Provider:點擊「Create」→「Provider name」,輸入名稱(例如:個人 AI 專案)。
  3. 建立 Channel
    • 選擇「Create a Messaging API channel」。
    • 會先要求新增官方帳號點擊「Create a LINE Offcial Account」
    • 建立LINE官方帳號 頁面 在依照自記填入資料
    • 您的LINE官方帳號已建立完成後選擇「稍後進行認證 ( 前往管理畫面 )」
    • 進入LINE官方帳號管理後台選擇「設定」>「Messaging API」>「啟用Messaging API」
  4. 取得關鍵金鑰 (請存入記事本)
    • 「啟用Messaging API」後頁面會顯示「Channel secret」複製起來
    • 回到 LINE Developers 選擇剛剛建立的「Provider」與「Channels」
    • 到「Messaging API」頁籤,點擊下面 Channel access token「lssue」。
    • (會出現很長一串亂碼這就是 Channel Access Token 複製起來)
LINE Developers Console Messaging API 分頁介面,紅色框選處展示 Channel access token 取得位置
關鍵步驟:在 Messaging API 分頁最下方取得 Channel access token (用於 GAS 串接)

Step 2: 取得 Gemini API Key

為了讓您的 AI 助理 變聰明,我們需要 Google Gemini 模型的支援。

  1. 前往 Google AI Studio
  2. 點擊「Create API key」→「Create API key in new project」。
  3. 複製生成的 API Key (開頭通常是 AIzaSy…)。
Google AI Studio 介面,紅色框選處展示 Create API key 按鈕與複製金鑰的位置
關鍵步驟:在 Google AI Studio 點擊 Create API key 並複製金鑰 (用於 AI 大腦串接)

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 (主程式邏輯)

Google Apps Script 開發介面,LINE Bot 後端邏輯核心
Google Apps Script:LINE Bot 的 Serverless 後端核心

這是 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。

  1. 設定環境變數:
    • 在 Apps Script 左側點擊「專案設定」(齒輪圖示)。
    • 新增以下「指令碼屬性」
      • LINE_CHANNEL_ACCESS_TOKEN: (從 Step 1 取得)
      • GEMINI_API_KEY: (從 Step 2 取得)
  2. 部署 Web App:
    • 點擊右上角「部署」→「新增部署作業」。
    • 類型選擇「網路應用程式」。
    • 具有存取權的使用者:務必選擇「所有人」(否則 LINE 無法呼叫)。
    • 點擊部署,複製生成的 Web App URL。
  3. 設定 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 技術轉化為實用的商業解決方案。所有教學程式碼均經過實驗室實際部署驗證。

AI Overview Local Pack 衝擊實戰分析:排名沒掉、電話卻消失的 3 大真相

你的本地搜尋排名穩坐前三,來電卻莫名下滑 30% 以上?真相是 AI Overview Local Pack 正在取代傳統的地圖三件組。2025 年底起 Google 在美國行動搜尋大規模部署這項 AI 驅動新版面,Sterling Sky 分析 322 個市場發現,88% 的市場中新版面顯示的商家數量比傳統 3-Pack 更少,部分商家能見度暴跌超過 50%。

< SYSTEM_READY />

需要專業的服務?

無論是網頁設計、系統開發或 GCP 雲端服務,我們都能提供最適合您的解決方案。

// WAITING_FOR_INPUT...