🚀 2026/0830 重磅升級 ‧ 3 分頁切換式旗艦選單

讓 LINE 教師管家更好用!
3 分頁旗艦圖文選單快速安裝與設定教學

完全免登入 LINE 後台、免手動拉框!透過 Google Apps Script 一鍵全自動安裝「3 分頁切換式旗艦圖文選單」,毫秒級流暢切換日常反查、事後改存與事前鎖定!

👉 還沒設定 LINE Bot 機器人嗎? 前往第一篇完整串接教學 ➔
LINE 教師智慧管家 主視覺
🤖

1. 全能教師智慧管家・指令與多模態秘笈

支援文字、語音直接說、拍照多模態上傳與聊天室快捷指令反查!

📸 【拍照與多模態上傳】
  • 拍名片 ➜ 自動解析姓名電話,存入試算表並同步 Google 聯絡人
  • 拍發票 / 收據 ➜ 自動解析金額與品項並存入記帳本
  • 拍研習海報 ➜ 自動擷取時間地點並同步 Google 日曆
  • 拍黑白板 / 筆記 ➜ 自動整理手寫重點並歸檔教案庫
🎙️ 【語音直錄】
  • 長按麥克風直接說話 ➜ Gemini AI 自動聽寫、結構化理解並分門別類存入 Google 試算表對應分頁
  • • 支援隨口念聯絡簿、通話後錄音備忘、口述學生衝突事件等

【快捷管理指令】

📋 教師智慧管家 ‧ 指令卡片盒 ↗
快捷指令 功能說明 快速複製
/安裝選單 全自動為 LINE 官方帳號安裝「3 分頁切換式旗艦圖文選單」
/網址 <網址> 綁定個人的 Google Apps Script Web App 專屬儀表板網址
/查 今日 查看今日聯絡簿公告與今日 Google 日曆行程
/查 進度 查看各班最新教學進度、章節紀錄與交代事項
/查 缺交 查看全班各科未補交作業清單與座號名單
/查 聯絡人 查詢通訊錄、電話、Email 與單位
/查 花費 查看記帳本本月累計總支出金額與最近 3 筆消費明細
/名片 、 /行事曆 事前鎖定 5 分鐘強制歸檔模式(拍照即刻精準分類)
改存 聯絡人 事後一鍵轉移上一筆紀錄至聯絡人(並自動清除舊紀錄)
/undo 撤回並刪除上一筆存入 Google 試算表的資料(誤記修正)
/help 顯示完整指令手冊與 11 大分類輸入範例指南
🚀
✨ 2026/0830 重磅升級 ‧ 推薦快速建立方式

2. 【快速建立方式】Google Apps Script 一鍵全自動安裝 3 分頁旗艦圖文選單

完全免登入 LINE 後台、免手動拉框!透過程式 3 秒自動註冊 3 大分頁、上傳立體晶透底圖並動態綁定個人專屬儀表板!

🌟 3 大分頁 ‧ 毫秒級無縫切換預覽

0
前置確認:檢查 3 大金鑰屬性已填妥

進入 Google 試算表 ➔「擴充功能 > Apps Script」➔ 點選左側齒輪 ⚙️ 「專案設定」 ➔ 拉到最下方「指令碼屬性」,確認以下 3 個項目皆已建立:

  • LINE_ACCESS_TOKEN:LINE Messaging API 的 Channel Access Token
  • GEMINI_API_KEY:Google AI Studio 的 API Key
  • WEB_APP_URL:自己部署出來的應用程式網址(結尾為 /exec
A
安裝方式 A:在 Apps Script 點擊執行(最推薦 ⭐⭐⭐⭐⭐)

在 Google Apps Script 編輯器畫面上方:

  1. 在上方函式下拉選單中,選取 setupMultiTabRichMenus
  2. 點擊 ▶️ 「執行」 按鈕。
  3. 等待 3~5 秒看到下方日誌顯示:
    🎉 恭喜!全套「3 分頁切換式旗艦圖文選單」已成功全自動安裝並發布!
  4. 打開手機 LINE:將與管家的聊天室關閉再重新點開,選單立即亮麗登場!
B
安裝方式 B:在手機 LINE 聊天室用指令安裝(免開電腦 ⭐⭐⭐⭐)

直接在與管家的 LINE 聊天室中:

  1. 傳送指令綁定網址:
    /網址 您的AppsScript網址(例如:/網址 https://script.google.com/macros/s/.../exec
  2. 接著傳送安裝指令:
    /安裝選單
  3. 收到管家回傳安裝成功訊息後,關閉聊天室再重新點開即刻生效!
🔄
🛡️ 安全無痛 ‧ 零資料遺失

3. 舊版學員 ‧ 4 步驟無痛升級 3 分頁旗艦版

原本試算表內的所有紀錄完全保留!只要 2 分鐘,無縫升級為最新 3 分頁圖文選單系統!

🛡️ 升級安全保證:
  • ✅ 原本試算表內的 11 大分頁資料 100% 完整保留
  • ✅ Google 聯絡人與日曆同步設定 不受任何影響
  • ✅ LINE Developers 後台的 Webhook 網址 完全不需要重新設定
1
步驟 1:貼上最新版 Code.gs 程式碼

打開您原本的 Google 試算表 ➔ 點選「擴充功能 > Apps Script」➔ 點進 Code.gs 編輯器:

  1. 在編輯器內按鍵盤 Ctrl + A(全選舊程式碼)➔ 按 Delete 清空。
  2. 點擊下方按鈕 【📋 一鍵複製完整 Code.gs 程式碼】,貼入編輯器中並按 儲存(Ctrl + S)
📄 Code.gs(最新旗艦版程式碼)
/**
 * ====================================================================
 * 【LINE 官方帳號 + Gemini AI + Google 試算表 全能教育與生活智慧管家】
 * 
 * 專為奕鈞老師與教師/導師量身打造的免伺服器雲端智慧系統
 * 支援 11+1 大智慧分類:
 * 1. 👤 聯絡人(拍名片/文字自動歸檔,並自動同步新增至 Google 聯絡人通訊錄!)
 * 2. 📖 教學進度(班級、章節進度、頁數、小老師交代事項)
 * 3. 📢 今日聯絡簿(自動產生一鍵轉發班群美化公告,亦可拍照辨識)
 * 4. 📝 作業缺交與追蹤(支援隨時下指令查詢未補交名單)
 * 5. 👨‍🎓 學生概況(支援主要學生 + 關聯同行學生)
 * 6. 📞 家長聯繫紀錄
 * 7. 💡 教學備課與靈感庫(支援文字、黑白板手寫筆記拍照辨識)
 * 8. 📅 行事曆待辦(支援文字、拍研習海報/簡章自動同步 Google 日曆)
 * 9. 💰 記帳本(支援文字、拍發票/收據拍照自動記帳)
 * 10. 📌 生活雜事備忘
 * 11. 🎁 好物介紹連結
 * 12. 💬 AI 備課/出題/專業諮詢
 * 
 * 🌟 旗艦進階功能:
 * • 📸 圖片多模態辨識(名片強制優先建檔、發票記帳、活動海報、手寫筆記)
 * • 🎯 事前強制鎖定模式(/名片、/行事曆、/記帳、/進度 等,5分鐘內強制歸類)
 * • 🔄 事後一鍵改存轉移(改存 聯絡人、改存 行事曆 等,自動轉存並清除舊紀錄)
 * • 🎙️ 語音直錄聽寫歸檔(LINE 語音訊息自動聽寫並分類)
 * • 📇 Google 聯絡人 (People API) 雲端通訊錄自動同步
 * • 🤖 動態模型切換(/model 查詢與切換指定模型)
 * • 🔍 聊天室反向搜尋(/查 聯絡人/進度/缺交/今日/花費)
 * • 🗑️ 精準刪除指定紀錄(/del <關鍵字> 快速刪除錯誤資料)
 * • ↩️ 一鍵撤回紀錄(/undo 刪除上一筆存入的資料)
 * • 🌅 每日早安晨報(定時排程主動推播當日行程與待辦)
 * 
 * 作者:奕鈞老師 (https://ijun-ai.com)
 * ====================================================================
 */

// ==================== 1. 基本設定區 ====================
var CONFIG = {
  LINE_ACCESS_TOKEN: PropertiesService.getScriptProperties().getProperty('LINE_ACCESS_TOKEN') || 'YOUR_LINE_ACCESS_TOKEN',
  GEMINI_API_KEY: PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY') || 'YOUR_GEMINI_API_KEY',
  SPREADSHEET_ID: PropertiesService.getScriptProperties().getProperty('SPREADSHEET_ID') || (function(){ try { return SpreadsheetApp.getActiveSpreadsheet().getId(); } catch(e) { return '1TSbmh4ANMilmyu5QWxS3j0nET3jfSFMzdLLPHupsAUc'; } })(),
  GEMINI_MODEL: 'gemini-1.5-flash',
  SHEET_NAMES: {
    CONTACTS: '聯絡人',
    TEACHING_PROGRESS: '教學進度',
    CONTACT_BOOK: '今日聯絡簿',
    HOMEWORK_MISSING: '作業缺交',
    STUDENT: '學生概況',
    PARENT_LOG: '家長聯繫',
    TEACHING_IDEA: '備課靈感',
    CALENDAR: '行事曆',
    EXPENSE: '記帳本',
    TASK: '生活雜事',
    GOODS: '好物分享'
  }
};

// ==================== 2. LINE Webhook 與 網頁儀表板入口 (doPost & doGet) ====================
function doPost(e) {
  if (!e || !e.postData) {
    return ContentService.createTextOutput(JSON.stringify({ status: 'no post data' })).setMimeType(ContentService.MimeType.JSON);
  }

  try {
    const postData = JSON.parse(e.postData.contents);
    const events = postData.events;

    if (!events || events.length === 0) {
      return ContentService.createTextOutput(JSON.stringify({ status: 'ok' })).setMimeType(ContentService.MimeType.JSON);
    }

    for (const event of events) {
      const replyToken = event.replyToken;
      const userId = event.source ? event.source.userId : null;
      
      if (userId) {
        PropertiesService.getScriptProperties().setProperty('ADMIN_USER_ID', userId);
      }

      if (event.type === 'message') {
        const msgType = event.message.type;

        // 1. 純文字訊息處理
        if (msgType === 'text') {
          const userText = String(event.message.text || '').trim();

          const commandReply = handleUserCommand(userText, userId);
          if (commandReply) {
            sendLineReply(replyToken, commandReply);
            continue;
          }

          const parsedResult = parseWithGemini(userText);
          const writeResult = saveToSpreadsheet(parsedResult, userText);
          const replyMessage = formatReplyMessage(parsedResult, writeResult);
          sendLineReply(replyToken, replyMessage);
        }
        
        // 2. 圖片訊息處理(名片辨識、發票記帳、海報行程、白板筆記、聯絡簿辨識)
        else if (msgType === 'image') {
          try {
            const imageBlob = getLineMessageContentBlob(event.message.id);
            if (!imageBlob || imageBlob.getBytes().length === 0) {
              sendLineReply(replyToken, '⚠️ 無法從 LINE 取得圖片內容,請確認 LINE_ACCESS_TOKEN 權限或稍後重試。');
              continue;
            }

            const base64Data = Utilities.base64Encode(imageBlob.getBytes());
            const mimeType = imageBlob.getContentType() || 'image/jpeg';

            // 檢查是否有使用者強制指定的分類模式
            let forceCategory = null;
            if (userId) {
              const forceJson = PropertiesService.getScriptProperties().getProperty('FORCE_MODE_' + userId);
              if (forceJson) {
                try {
                  const forceObj = JSON.parse(forceJson);
                  if (forceObj.expireAt > Date.now()) {
                    forceCategory = forceObj.category;
                  } else {
                    PropertiesService.getScriptProperties().deleteProperty('FORCE_MODE_' + userId);
                  }
                } catch(e) {}
              }
            }

            const parsedResult = parseWithGeminiMultimodal({
              type: 'image',
              mimeType: mimeType,
              base64Data: base64Data,
              forceCategory: forceCategory
            });

            if (forceCategory) {
              parsedResult.category = forceCategory;
              PropertiesService.getScriptProperties().deleteProperty('FORCE_MODE_' + userId);
            }

            const writeResult = saveToSpreadsheet(parsedResult, '[名片/圖片多模態辨識]');
            const replyMessage = '📸 【圖片多模態智慧辨識完成】\n' + formatReplyMessage(parsedResult, writeResult);
            sendLineReply(replyToken, replyMessage);
          } catch (imgErr) {
            console.error('圖片辨識處理失敗:', imgErr);
            sendLineReply(replyToken, '⚠️ 圖片辨識失敗:' + imgErr.toString() + '\n💡 請確認:\n1. 試算表指令碼屬性 LINE_ACCESS_TOKEN 是否正確\n2. 是否已完成 Google 帳號授權\n3. 圖片是否過大');
          }
        }

        // 3. 語音訊息處理
        else if (msgType === 'audio') {
          try {
            const audioBlob = getLineMessageContentBlob(event.message.id);
            if (!audioBlob || audioBlob.getBytes().length === 0) {
              sendLineReply(replyToken, '⚠️ 無法從 LINE 取得語音檔案,請確認 LINE_ACCESS_TOKEN 權限或稍後重試。');
              continue;
            }

            const base64Data = Utilities.base64Encode(audioBlob.getBytes());
            let mimeType = audioBlob.getContentType() || 'audio/mp4';
            if (!mimeType.includes('/')) mimeType = 'audio/mp4';

            const parsedResult = parseWithGeminiMultimodal({
              type: 'audio',
              mimeType: mimeType,
              base64Data: base64Data
            });

            const writeResult = saveToSpreadsheet(parsedResult, '[語音多模態辨識]');
            const replyMessage = '🎙️ 【語音直錄辨識歸檔完成】\n' + formatReplyMessage(parsedResult, writeResult);
            sendLineReply(replyToken, replyMessage);
          } catch (audErr) {
            console.error('語音辨識處理失敗:', audErr);
            sendLineReply(replyToken, '⚠️ 語音辨識失敗:' + audErr.toString() + '\n💡 請確認錄音長度是否適中,以及 LINE_ACCESS_TOKEN 是否正確設定。');
          }
        }
      }
    }

    return ContentService.createTextOutput(JSON.stringify({ status: 'success' })).setMimeType(ContentService.MimeType.JSON);
  } catch (err) {
    console.error('doPost 發生錯誤:', err);
    return ContentService.createTextOutput(JSON.stringify({ status: 'error', message: err.toString() })).setMimeType(ContentService.MimeType.JSON);
  }
}

// 🌐 網頁端入口
function doGet(e) {
  if (e && e.parameter && e.parameter.action === 'get_data') {
    const allData = getAllSheetRecords();
    return ContentService.createTextOutput(JSON.stringify(allData)).setMimeType(ContentService.MimeType.JSON);
  }

  const htmlOutput = HtmlService.createHtmlOutput(renderDashboardHtml())
    .setTitle('全能教師智慧管家・個人雲端儀表板')
    .addMetaTag('viewport', 'width=device-width, initial-scale=1.0')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
  return htmlOutput;
}

// 讀取試算表所有分頁的資料
function getAllSheetRecords() {
  const ssId = CONFIG.SPREADSHEET_ID;
  const result = { sheets: {}, stats: { totalExpense: 0, missingCount: 0, todayContact: null, contactCount: 0 } };
  
  try {
    let ss = null;
    if (ssId && ssId !== 'YOUR_SPREADSHEET_ID') { try { ss = SpreadsheetApp.openById(ssId); } catch(e){} }
    if (!ss) { try { ss = SpreadsheetApp.getActiveSpreadsheet(); } catch(e){} }
    if (!ss) return result;
    
    ensureSheetHeaders(ss);
    
    Object.keys(CONFIG.SHEET_NAMES).forEach(key => {
      const sheetName = CONFIG.SHEET_NAMES[key];
      const sheet = ss.getSheetByName(sheetName);
      if (sheet) {
        // 使用 getDisplayValues() 確保所有欄位皆為純字串,避免 Date 導致 GAS 序列化失敗
        const data = sheet.getDataRange().getDisplayValues();
        if (data.length > 1) {
          const headers = data[0];
          const rowsWithMeta = [];
          for (let r = 1; r < data.length; r++) {
            rowsWithMeta.push({
              actualRow: r + 1,
              values: data[r]
            });
          }
          rowsWithMeta.reverse(); // 最新在最上面
          result.sheets[sheetName] = { headers: headers, rows: rowsWithMeta };

          if (sheetName === CONFIG.SHEET_NAMES.EXPENSE) {
            rowsWithMeta.forEach(item => {
              if (item.values[1] === '支出') {
                const num = parseFloat(String(item.values[4] || '').replace(/[^0-9.-]/g, '')) || 0;
                result.stats.totalExpense += num;
              }
            });
          }
          if (sheetName === CONFIG.SHEET_NAMES.HOMEWORK_MISSING) {
            rowsWithMeta.forEach(item => {
              if (item.values[4] === '未補交' || item.values[4] === '待追蹤') result.stats.missingCount++;
            });
          }
          if (sheetName === CONFIG.SHEET_NAMES.CONTACTS) {
            result.stats.contactCount = rowsWithMeta.length;
          }
          if (sheetName === CONFIG.SHEET_NAMES.CONTACT_BOOK && rowsWithMeta.length > 0) {
            result.stats.todayContact = rowsWithMeta[0].values;
          }
        } else {
          result.sheets[sheetName] = { headers: data[0] || [], rows: [] };
        }
      }
    });
  } catch (err) {
    console.error('讀取試算表資料異常:', err);
  }
  return result;
}

// ==================== 3. LINE 聊天室快捷指令處理核心 ====================
function handleUserCommand(text, userId) {
  if (!text) return null;
  const lower = String(text).toLowerCase().trim();

  // 🎯 【事前強制指定分類模式】(例如:/聯絡人、/名片、/行事曆、/記帳、/進度 等)
  const forceMap = {
    '/聯絡人': '聯絡人', '/名片': '聯絡人', '強制聯絡人': '聯絡人', '強制 聯絡人': '聯絡人',
    '/行事曆': '行事曆', '/日曆': '行事曆', '強制行事曆': '行事曆', '強制 行事曆': '行事曆',
    '/記帳': '記帳', '/發票': '記帳', '強制記帳': '記帳', '強制 記帳': '記帳',
    '/進度': '教學進度', '強制進度': '教學進度', '強制 進度': '教學進度',
    '/聯絡簿': '今日聯絡簿', '強制聯絡簿': '今日聯絡簿', '強制 聯絡簿': '今日聯絡簿',
    '/靈感': '備課靈感', '強制靈感': '備課靈感', '強制 靈感': '備課靈感',
    '/缺交': '作業缺交', '強制缺交': '作業缺交', '強制 缺交': '作業缺交',
    '/學生': '學生概況', '強制學生': '學生概況', '強制 學生': '學生概況',
    '/家長': '家長聯繫', '強制家長': '家長聯繫', '強制 家長': '家長聯繫',
    '/雜事': '生活雜事', '強制雜事': '生活雜事', '強制 雜事': '生活雜事',
    '/好物': '好物分享', '強制好物': '好物分享', '強制 好物': '好物分享'
  };

  if (userId && forceMap[lower]) {
    const targetCat = forceMap[lower];
    const forceObj = { category: targetCat, expireAt: Date.now() + 5 * 60 * 1000 };
    PropertiesService.getScriptProperties().setProperty('FORCE_MODE_' + userId, JSON.stringify(forceObj));
    return '🎯 【已鎖定強制「' + targetCat + '」模式】\n━━━━━━━━━━━━━━\n📷 請立即拍照或上傳圖片/文字(5 分鐘內有效),管家將 100% 強制歸檔至「' + targetCat + '」分頁!\n\n💡 若想取消鎖定請輸入 /cancel';
  }

  if (userId && (lower === '/cancel' || lower === '取消' || lower === '取消強制')) {
    PropertiesService.getScriptProperties().deleteProperty('FORCE_MODE_' + userId);
    return '🔄 已取消強制指定模式,恢復為 AI 自動智慧判斷!';
  }

  // 🔄 【事後一鍵改存/轉移分類】(例如:改存 聯絡人、轉存 行事曆、/改 記帳)
  if (lower.startsWith('改存') || lower.startsWith('轉存') || lower.startsWith('/改') || lower.startsWith('/to')) {
    const targetKw = text.replace(/^(改存|轉存|\/改|\/to)\s*/i, '').trim();
    const catKeys = Object.values(CONFIG.SHEET_NAMES);
    let matchedCat = catKeys.find(c => targetKw.includes(c) || c.includes(targetKw));
    if (targetKw.includes('名片') || targetKw.includes('電話')) matchedCat = '聯絡人';
    if (targetKw.includes('日曆') || targetKw.includes('行程')) matchedCat = '行事曆';
    if (targetKw.includes('發票') || targetKw.includes('開銷')) matchedCat = '記帳本';
    if (targetKw.includes('進度')) matchedCat = '教學進度';
    if (targetKw.includes('聯絡簿')) matchedCat = '今日聯絡簿';
    if (targetKw.includes('靈感')) matchedCat = '備課靈感';

    if (!matchedCat) {
      return '🔄 請指定要轉存的分類名稱,例如:\n• 改存 聯絡人\n• 改存 行事曆\n• 改存 記帳\n• 改存 教學進度';
    }

    const lastRecordJson = PropertiesService.getScriptProperties().getProperty('LAST_SAVED_RECORD');
    if (!lastRecordJson) {
      return '⚠️ 目前沒有上一筆紀錄可供轉存。';
    }

    try {
      const lastRecord = JSON.parse(lastRecordJson);
      const ss = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID);
      const oldSheet = ss.getSheetByName(lastRecord.sheetName);
      let originalContent = lastRecord.summary || '';

      if (oldSheet && lastRecord.row <= oldSheet.getLastRow()) {
        const rowValues = oldSheet.getRange(lastRecord.row, 1, 1, oldSheet.getLastColumn()).getValues()[0];
        originalContent = rowValues.slice(1).filter(Boolean).join(' ');
        oldSheet.deleteRow(lastRecord.row);
      }

      const forcedPrompt = '請將以下內容強制解析歸類為「' + matchedCat + '」分類,並提取對應欄位:\n' + originalContent;
      const reParsed = parseWithGemini(forcedPrompt);
      reParsed.category = (matchedCat === '記帳本' ? '記帳' : matchedCat);
      
      const writeResult = saveToSpreadsheet(reParsed, originalContent);
      return '🔄 【已成功轉存至「' + writeResult.sheetName + '」】\n━━━━━━━━━━━━━━\n• 舊分頁「' + lastRecord.sheetName + '」紀錄已自動清除\n• ' + formatReplyMessage(reParsed, writeResult);
    } catch (reErr) {
      return '⚠️ 轉存失敗:' + reErr.message;
    }
  }

  // 🌟 圖文選單管理指令 (/安裝選單, /移除選單, /儀表板)
  if (lower === '/安裝選單' || lower === '/選單安裝' || lower === '/menu' || lower === '/setup-menu' || lower === '安裝選單') {
    try {
      const res = setupMultiTabRichMenus();
      return res.message;
    } catch (menuErr) {
      return '⚠️ 安裝圖文選單失敗:' + menuErr.message + '\n💡 請確認 LINE_ACCESS_TOKEN 是否已正確填入指令碼屬性中。';
    }
  }

  if (lower === '/移除選單' || lower === '/刪除選單' || lower === '/delete-menu' || lower === '移除選單') {
    try {
      const res = deleteMultiTabRichMenus();
      return res.message;
    } catch (delErr) {
      return '⚠️ 移除選單失敗:' + delErr.message;
    }
  }

  // 🌐 設定或查詢專屬儀表板網址 (/網址, /設定網址, /儀表板)
  if (lower.startsWith('/網址') || lower.startsWith('/設定網址') || lower.startsWith('/url') || lower.startsWith('/seturl') || lower === '/儀表板' || lower === '/後台' || lower === '儀表板' || lower === '後台') {
    const parts = text.split(/\s+/);
    if (parts.length > 1 && parts[1].startsWith('http')) {
      const newUrl = parts[1].trim();
      PropertiesService.getScriptProperties().setProperty('WEB_APP_URL', newUrl);
      try {
        setupMultiTabRichMenus();
        return '🎉 【儀表板網址已成功綁定並更新選單!】\n━━━━━━━━━━━━━━\n🔗 最新專屬網址:\n' + newUrl + '\n━━━━━━━━━━━━━━\n📱 圖文選單的「🌐 個人儀表板」按鈕已即時同步更新!';
      } catch (e) {
        return '✅ 網址已儲存至系統!\n🔗 ' + newUrl + '\n💡 請重新輸入 /安裝選單 即可同步至圖文選單按鈕!';
      }
    }

    let currentUrl = PropertiesService.getScriptProperties().getProperty('WEB_APP_URL');
    if (!currentUrl) {
      try { currentUrl = ScriptApp.getService().getUrl(); } catch(e) {}
    }
    if (!currentUrl || !currentUrl.startsWith('http')) {
      currentUrl = 'https://ijun-ai.com/tutor/line-bot/';
    }

    return '🌐 【您的專屬個人雲端儀表板】\n━━━━━━━━━━━━━━\n👉 點此開啟:\n' + currentUrl + '\n━━━━━━━━━━━━━━\n💡 如何更換為自己的 Apps Script 網頁?\n只要在 LINE 輸入:\n/網址 <貼上您的 Apps Script 網址>\n(例如:/網址 https://script.google.com/macros/s/.../exec)\n管家就會自動將圖文選單按鈕換成您的專屬網址!';
  }

  // 1. 模型查詢與切換指令 (/model)
  if (lower.startsWith('/model') || lower.startsWith('模型') || lower.startsWith('查模型')) {
    const parts = text.split(/\s+/);
    const manualModel = PropertiesService.getScriptProperties().getProperty('GEMINI_MODEL');

    if (parts.length === 1 || parts[1] === 'list' || parts[1] === '清單') {
      const candidates = getCandidateModels(CONFIG.GEMINI_API_KEY);
      let reply = '🤖 【Gemini 模型設定狀態】\n━━━━━━━━━━━━━━\n';
      if (manualModel) {
        reply += '📌 當前模式:手動鎖定\n🎯 使用模型:' + manualModel + '\n\n';
      } else {
        reply += '📌 當前模式:✨ 智慧動態輪詢(自動推薦最佳模型)\n🎯 優先模型:' + candidates[0] + '\n\n';
      }
      reply += '📋 推薦可用候選清單:\n';
      candidates.slice(0, 5).forEach((m, idx) => {
        reply += (idx + 1) + '. ' + m + (m === (manualModel || candidates[0]) ? ' (使用中)' : '') + '\n';
      });
      reply += '━━━━━━━━━━━━━━\n💡 切換指令:\n• 手動切換:輸入 「/model <模型名>」\n  (例: /model gemini-2.5-pro)\n• 恢復自動:輸入 「/model auto」';
      return reply;
    }

    const targetModel = parts[1].trim();
    if (targetModel.toLowerCase() === 'auto' || targetModel.toLowerCase() === 'reset' || targetModel === '自動' || targetModel === '預設') {
      PropertiesService.getScriptProperties().deleteProperty('GEMINI_MODEL');
      return '🔄 已成功恢復為【✨ 智慧動態輪詢模式】!\n系統將自動探測並優先調用最新、最穩定的 Flash 模型。';
    }

    PropertiesService.getScriptProperties().setProperty('GEMINI_MODEL', targetModel);
    return '✅ 已成功切換指定模型為:\n🎯 「' + targetModel + '」\n\n下一則訊息將立即以此模型進行分析與辨識!';
  }

  // 2. 撤銷/刪除上一筆紀錄 (/undo)
  if (lower === '/undo' || lower === '/撤銷' || lower === '/撤回' || lower === '撤回上一筆' || lower === '刪除上一筆') {
    const lastRecordJson = PropertiesService.getScriptProperties().getProperty('LAST_SAVED_RECORD');
    if (!lastRecordJson) {
      return '⚠️ 目前沒有可撤回的紀錄(或已撤回過)。';
    }

    try {
      const lastRecord = JSON.parse(lastRecordJson);
      const ss = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID);
      const sheet = ss.getSheetByName(lastRecord.sheetName);
      if (sheet && lastRecord.row <= sheet.getLastRow()) {
        sheet.deleteRow(lastRecord.row);
        PropertiesService.getScriptProperties().deleteProperty('LAST_SAVED_RECORD');
        return '🗑️ 【已成功撤回上一筆紀錄】\n━━━━━━━━━━━━━━\n• 來源分頁:' + lastRecord.sheetName + '\n• 刪除內容:' + (lastRecord.summary || '已清除') + '\n━━━━━━━━━━━━━━\n✨ 試算表已完成同步刪除!';
      } else {
        return '⚠️ 找不到對應的資料列,可能已手動編輯過。';
      }
    } catch (e) {
      return '⚠️ 撤回失敗:' + e.message;
    }
  }

  // 3. 🎯 精準搜尋並刪除指定紀錄 (/del <關鍵字> 或 /刪除 <關鍵字>)
  if (lower.startsWith('/del') || lower.startsWith('/delete') || lower.startsWith('/刪除') || text.startsWith('刪除 ')) {
    const kw = text.replace(/^(\/del|\/delete|\/刪除|刪除)\s*/i, '').trim();
    if (!kw) {
      return '🗑️ 請提供想刪除的關鍵字,例如:\n• /del 205 (刪除包含 205 的最新紀錄)\n• /del 王大明 (刪除聯絡人或紀錄)\n• /del 排骨便當 (刪除該筆記帳)';
    }

    const ss = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID);
    const searchSheets = [
      CONFIG.SHEET_NAMES.CONTACTS,
      CONFIG.SHEET_NAMES.TEACHING_PROGRESS,
      CONFIG.SHEET_NAMES.EXPENSE,
      CONFIG.SHEET_NAMES.HOMEWORK_MISSING,
      CONFIG.SHEET_NAMES.STUDENT,
      CONFIG.SHEET_NAMES.PARENT_LOG,
      CONFIG.SHEET_NAMES.CONTACT_BOOK,
      CONFIG.SHEET_NAMES.CALENDAR,
      CONFIG.SHEET_NAMES.TASK,
      CONFIG.SHEET_NAMES.TEACHING_IDEA,
      CONFIG.SHEET_NAMES.GOODS
    ];

    let foundSheet = null;
    let foundRowIndex = -1;
    let foundContent = '';
    let foundTime = '';

    for (const sName of searchSheets) {
      const sheet = ss.getSheetByName(sName);
      if (sheet && sheet.getLastRow() > 1) {
        const data = sheet.getDataRange().getValues();
        for (let i = data.length - 1; i >= 1; i--) {
          const rowStr = data[i].join(' ');
          if (rowStr.toLowerCase().includes(kw.toLowerCase())) {
            foundSheet = sheet;
            foundRowIndex = i + 1;
            foundTime = String(data[i][0] || '').substring(0, 16);
            foundContent = data[i].slice(1).filter(Boolean).join(' | ').substring(0, 40);
            break;
          }
        }
      }
      if (foundSheet) break;
    }

    if (foundSheet && foundRowIndex > 0) {
      foundSheet.deleteRow(foundRowIndex);
      return '🗑️ 【已成功精準刪除 1 筆紀錄】\n━━━━━━━━━━━━━━\n• 來源分頁:' + foundSheet.getName() + '\n• 記錄時間:' + (foundTime || '無') + '\n• 刪除內容:' + foundContent + '...\n━━━━━━━━━━━━━━\n✨ 試算表已同步即時刪除!';
    } else {
      return '🔍 查無包含關鍵字「' + kw + '」的紀錄,未執行任何刪除。';
    }
  }

  // 4. 跨分頁與特定主題快速反向查詢 (/查)
  if (lower.startsWith('/查') || lower.startsWith('/find') || lower.startsWith('/search') || text.startsWith('查 ')) {
    const kw = text.replace(/^(\/查|\/find|\/search|查)\s*/i, '').trim();
    if (!kw) {
      return '🔍 請提供想查詢的關鍵字,例如:\n• /查 聯絡人 (或 /查 王大明)\n• /查 進度 (或 /查 205)\n• /查 缺交\n• /查 今日\n• /查 記帳';
    }

    const ss = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID);

    // 1. 聯絡人通訊錄查詢
    if (kw === '聯絡人' || kw === '通訊錄' || kw === '名片' || kw === '電話') {
      const sheet = ss.getSheetByName(CONFIG.SHEET_NAMES.CONTACTS);
      if (!sheet || sheet.getLastRow() <= 1) return '👤 目前聯絡人通訊錄尚無紀錄。';
      const data = sheet.getDataRange().getValues();
      const list = [];
      for (let i = data.length - 1; i >= 1; i--) {
        const row = data[i];
        list.push('• 【' + row[1] + '】' + (row[4] ? row[4] + ' | ' : '') + '📞 ' + (row[2] || '無電話') + (row[3] ? ' | ✉️ ' + row[3] : ''));
        if (list.length >= 5) break;
      }
      return '👤 【最新聯絡人通訊錄(共 ' + (data.length - 1) + ' 位)】\n━━━━━━━━━━━━━━\n' + list.join('\n') + '\n━━━━━━━━━━━━━━\n💡 輸入「/查 <姓名>」可快速搜尋特定對象。';
    }

    // 2. 教學進度查詢
    if (kw === '進度' || kw === '教學進度' || kw === '進度表') {
      const sheet = ss.getSheetByName(CONFIG.SHEET_NAMES.TEACHING_PROGRESS);
      if (!sheet || sheet.getLastRow() <= 1) return '📖 目前教學進度表尚無紀錄。';
      const data = sheet.getDataRange().getValues();
      const progressList = [];
      for (let i = data.length - 1; i >= 1; i--) {
        const row = data[i];
        progressList.push('• 【' + row[1] + '】' + (row[2] || '') + ' ' + row[3] + ' (' + (row[4] || '無交代事項') + ')');
        if (progressList.length >= 5) break;
      }
      return '📖 【最新教學進度紀錄(最新 5 筆)】\n━━━━━━━━━━━━━━\n' + progressList.join('\n') + '\n━━━━━━━━━━━━━━\n💡 點擊儀表板可查看完整進度表。';
    }

    // 3. 作業缺交名單查詢
    if (kw === '缺交' || kw === '作業缺交' || kw === '作業') {
      const sheet = ss.getSheetByName(CONFIG.SHEET_NAMES.HOMEWORK_MISSING);
      if (!sheet) return '⚠️ 找不到「作業缺交」分頁。';
      const data = sheet.getDataRange().getValues();
      const unsubmitted = [];
      for (let i = 1; i < data.length; i++) {
        const row = data[i];
        if (row[4] === '未補交' || row[4] === '待追蹤') {
          unsubmitted.push('• 【' + row[1] + '】座號/名單:' + row[2] + ' (' + (row[3] || '無備註') + ')');
        }
      }
      if (unsubmitted.length === 0) {
        return '🎉 太棒了!目前「作業缺交」中沒有任何未補交的紀錄!';
      }
      return '📝 【目前未補交作業清單(共 ' + unsubmitted.length + ' 筆)】\n━━━━━━━━━━━━━━\n' + unsubmitted.slice(0, 10).join('\n') + '\n━━━━━━━━━━━━━━\n💡 點擊儀表板可查看完整紀錄。';
    }

    // 4. 今日總覽速報(今日聯絡簿 + 今日日曆行程)
    if (kw === '今日' || kw === '今天' || kw === '今日聯絡簿' || kw === '聯絡簿') {
      const cbSheet = ss.getSheetByName(CONFIG.SHEET_NAMES.CONTACT_BOOK);
      const calSheet = ss.getSheetByName(CONFIG.SHEET_NAMES.CALENDAR);
      let reply = '📢 【今日聯絡簿 & 概況速報】\n━━━━━━━━━━━━━━\n';

      if (cbSheet && cbSheet.getLastRow() > 1) {
        const cbData = cbSheet.getDataRange().getValues();
        const lastCb = cbData[cbData.length - 1];
        reply += '📚 【今日作業】\n' + (lastCb[1] || '無') + '\n\n🎒 【攜帶物品】\n' + (lastCb[2] || '無') + '\n\n📄 【通知單/回條】\n' + (lastCb[3] || '無') + '\n\n❤️ 【導師叮嚀】\n' + (lastCb[4] || '無') + '\n\n';
      } else {
        reply += '(今日尚無聯絡簿紀錄)\n\n';
      }

      if (calSheet && calSheet.getLastRow() > 1) {
        const calData = calSheet.getDataRange().getValues();
        const todayStr = Utilities.formatDate(new Date(), 'Asia/Taipei', 'yyyy/MM/dd');
        const todayEvents = [];
        for (let i = 1; i < calData.length; i++) {
          const row = calData[i];
          const timeStr = String(row[2] || '');
          if (timeStr.includes(todayStr) || row[5] !== '已完成') {
            todayEvents.push('• ' + row[1] + ' (' + timeStr + ') - ' + (row[3] || '無地點'));
          }
        }
        if (todayEvents.length > 0) {
          reply += '📅 【近期/今日待辦行程】\n' + todayEvents.slice(0, 5).join('\n') + '\n';
        }
      }
      reply += '━━━━━━━━━━━━━━\n✨ 隨手記、隨時查!';
      return reply;
    }

    // 5. 家長聯繫紀錄查詢
    if (kw === '家長' || kw === '家長聯繫' || kw === '聯繫' || kw === '訪談' || kw === '家長紀錄') {
      const sheet = ss.getSheetByName(CONFIG.SHEET_NAMES.PARENT_LOG);
      if (!sheet || sheet.getLastRow() <= 1) return '📞 目前「家長聯繫」尚無紀錄。';
      const data = sheet.getDataRange().getValues();
      const list = [];
      for (let i = data.length - 1; i >= 1; i--) {
        const row = data[i];
        list.push('• 【' + row[1] + '(' + (row[2] || '家長') + ')】' + (row[3] ? ' [' + row[3] + '] ' : '') + String(row[4] || '').substring(0, 25) + '... (追蹤:' + (row[5] || '無') + ')');
        if (list.length >= 5) break;
      }
      return '📞 【最新家長聯繫紀錄(最新 5 筆)】\n━━━━━━━━━━━━━━\n' + list.join('\n') + '\n━━━━━━━━━━━━━━\n💡 輸入「/查 <學生姓名>」可查特定學生所有通話紀錄。';
    }

    // 6. 備課靈感庫查詢
    if (kw === '備課' || kw === '備課靈感' || kw === '靈感' || kw === '教案' || kw === '靈感庫') {
      const sheet = ss.getSheetByName(CONFIG.SHEET_NAMES.TEACHING_IDEA);
      if (!sheet || sheet.getLastRow() <= 1) return '💡 目前「備課靈感庫」尚無紀錄。';
      const data = sheet.getDataRange().getValues();
      const list = [];
      for (let i = data.length - 1; i >= 1; i--) {
        const row = data[i];
        list.push('• 【' + (row[1] ? row[1] + ' - ' : '') + row[2] + '】' + String(row[3] || '').substring(0, 30) + '... (器材:' + (row[4] || '無') + ')');
        if (list.length >= 5) break;
      }
      return '💡 【最新備課靈感庫(最新 5 筆)】\n━━━━━━━━━━━━━━\n' + list.join('\n') + '\n━━━━━━━━━━━━━━\n💡 點擊儀表板可查看完整教案架構。';
    }

    // 7. 學生概況與事件查詢
    if (kw === '學生' || kw === '學生概況' || kw === '概況' || kw === '衝突' || kw === '常規') {
      const sheet = ss.getSheetByName(CONFIG.SHEET_NAMES.STUDENT);
      if (!sheet || sheet.getLastRow() <= 1) return '👨‍🎓 目前「學生概況」尚無紀錄。';
      const data = sheet.getDataRange().getValues();
      const list = [];
      for (let i = data.length - 1; i >= 1; i--) {
        const row = data[i];
        list.push('• 【' + row[1] + (row[2] && row[2] !== '無' ? ' & ' + row[2] : '') + '】[' + (row[3] || '事件') + '] ' + String(row[4] || '').substring(0, 25) + '... (' + String(row[0]).substring(0, 10) + ')');
        if (list.length >= 5) break;
      }
      return '👨‍🎓 【最新學生概況紀錄(最新 5 筆)】\n━━━━━━━━━━━━━━\n' + list.join('\n') + '\n━━━━━━━━━━━━━━\n💡 輸入「/查 <學生姓名>」可查特定學生所有事件紀錄。';
    }

    // 8. 行事曆與活動行程查詢
    if (kw === '行事曆' || kw === '行程' || kw === '日曆' || kw === '活動') {
      const sheet = ss.getSheetByName(CONFIG.SHEET_NAMES.CALENDAR);
      if (!sheet || sheet.getLastRow() <= 1) return '📅 目前行事曆尚無活動紀錄。';
      const data = sheet.getDataRange().getValues();
      const list = [];
      for (let i = data.length - 1; i >= 1; i--) {
        const row = data[i];
        list.push('• 【' + row[1] + '】時間:' + (row[2] || '未定') + (row[3] ? ' | 地點:' + row[3] : '') + ' [' + (row[5] || '進行中') + ']');
        if (list.length >= 5) break;
      }
      return '📅 【行事曆活動紀錄(最新 5 筆)】\n━━━━━━━━━━━━━━\n' + list.join('\n') + '\n━━━━━━━━━━━━━━\n✨ 已同步至您的 Google 日曆!';
    }

    // 9. 生活雜事待辦查詢
    if (kw === '雜事' || kw === '生活雜事' || kw === '待辦' || kw === '備忘' || kw === '代辦') {
      const sheet = ss.getSheetByName(CONFIG.SHEET_NAMES.TASK);
      if (!sheet || sheet.getLastRow() <= 1) return '📌 目前生活雜事待辦清單尚無紀錄。';
      const data = sheet.getDataRange().getValues();
      const list = [];
      for (let i = data.length - 1; i >= 1; i--) {
        const row = data[i];
        list.push('• 【' + row[1] + '】' + (row[2] ? row[2] + ' ' : '') + '[' + (row[4] || '待辦') + ']');
        if (list.length >= 5) break;
      }
      return '📌 【生活雜事待辦清單(最新 5 筆)】\n━━━━━━━━━━━━━━\n' + list.join('\n') + '\n━━━━━━━━━━━━━━\n💡 點擊儀表板可一鍵切換完成狀態。';
    }

    // 10. 好物分享推薦查詢
    if (kw === '好物' || kw === '好物分享' || kw === '推薦' || kw === '書籍' || kw === '工具') {
      const sheet = ss.getSheetByName(CONFIG.SHEET_NAMES.GOODS);
      if (!sheet || sheet.getLastRow() <= 1) return '🎁 目前好物分享庫尚無紀錄。';
      const data = sheet.getDataRange().getValues();
      const list = [];
      for (let i = data.length - 1; i >= 1; i--) {
        const row = data[i];
        list.push('• 【' + row[1] + '】' + (row[3] ? row[3] + ' ' : '') + (row[2] && row[2] !== '無' ? '🔗 ' + row[2] : ''));
        if (list.length >= 5) break;
      }
      return '🎁 【好物書籍與工具推薦(最新 5 筆)】\n━━━━━━━━━━━━━━\n' + list.join('\n') + '\n━━━━━━━━━━━━━━';
    }

    // 11. 記帳本支出統計查詢
    if (kw === '花費' || kw === '記帳' || kw === '支出' || kw === '開銷' || kw === '記帳本') {
      const expSheet = ss.getSheetByName(CONFIG.SHEET_NAMES.EXPENSE);
      if (!expSheet || expSheet.getLastRow() <= 1) return '💰 目前記帳本尚無紀錄。';
      const data = expSheet.getDataRange().getValues();
      let total = 0;
      const recent = [];
      for (let i = data.length - 1; i >= 1; i--) {
        const r = data[i];
        if (r[1] === '支出') {
          total += (Number(r[4]) || 0);
          if (recent.length < 3) {
            recent.push('• ' + r[3] + ':$' + r[4] + ' (' + r[2] + ')');
          }
        }
      }
      return '💰 【記帳本支出統計】\n━━━━━━━━━━━━━━\n💵 累計總支出:$' + total.toLocaleString() + ' 元\n\n🕒 最近 3 筆消費:\n' + recent.join('\n') + '\n━━━━━━━━━━━━━━';
    }

    // 12. 跨分頁模糊關鍵字搜尋
    const searchSheets = [
      CONFIG.SHEET_NAMES.CONTACTS,
      CONFIG.SHEET_NAMES.TEACHING_PROGRESS,
      CONFIG.SHEET_NAMES.STUDENT,
      CONFIG.SHEET_NAMES.PARENT_LOG,
      CONFIG.SHEET_NAMES.HOMEWORK_MISSING,
      CONFIG.SHEET_NAMES.TEACHING_IDEA,
      CONFIG.SHEET_NAMES.CALENDAR,
      CONFIG.SHEET_NAMES.TASK,
      CONFIG.SHEET_NAMES.GOODS
    ];

    const matched = [];
    searchSheets.forEach(sName => {
      const sheet = ss.getSheetByName(sName);
      if (sheet && sheet.getLastRow() > 1) {
        const data = sheet.getDataRange().getValues();
        for (let i = data.length - 1; i >= 1; i--) {
          const rowStr = data[i].join(' ');
          if (rowStr.toLowerCase().includes(kw.toLowerCase())) {
            matched.push('📑 [' + sName + '] ' + String(data[i][1] || '').substring(0, 20) + '... (' + String(data[i][0]).substring(0, 10) + ')');
            if (matched.length >= 6) break;
          }
        }
      }
    });

    if (matched.length === 0) {
      return '🔍 查無包含「' + kw + '」的相關紀錄。\n\n💡 專屬分類反查指令:\n• /查 今日(聯絡簿與日曆)\n• /查 家長(聯繫訪談紀錄)\n• /查 備課(教學靈感庫)\n• /查 聯絡人(通訊錄)\n• /查 學生(概況與衝突)\n• /查 行事曆(行程活動)\n• /查 雜事(待辦清單)\n• /查 進度(教學進度)\n• /查 缺交(作業缺交)\n• /查 花費(支出記帳)';
    }
    return '🔍 【搜尋「' + kw + '」結果(前 ' + matched.length + ' 筆)】\n━━━━━━━━━━━━━━\n' + matched.join('\n') + '\n━━━━━━━━━━━━━━\n💡 請在瀏覽器開啟儀表板查看完整明細。';
  }

  // 5. 指令說明手冊 (/help)
  if (lower === '/help' || lower === '/說明' || lower === '/指令' || lower === 'help' || lower === '選單') {
    return '🤖 【全能教師智慧管家・指令與多模態秘笈】\n━━━━━━━━━━━━━━\n' +
      '📸 【拍照與多模態上傳】\n' +
      '• 拍名片 ➜ 自動解析姓名電話,同步存入 Google 聯絡人\n' +
      '• 拍發票/收據 ➜ 自動解析金額並記帳\n' +
      '• 拍研習海報 ➜ 自動擷取時間同步 Google 日曆\n' +
      '• 拍黑白板/筆記 ➜ 自動整理重點歸檔\n\n' +
      '🎙️ 【語音直錄】\n' +
      '• 長按麥克風直接說話 ➜ 免打字自動聽寫歸檔\n\n' +
      '⚡ 【快捷管理指令】\n' +
      '• /名片 或 /聯絡人 ➜ 鎖定強制聯絡人模式\n' +
      '• /日曆 或 /行事曆 ➜ 鎖定強制行事曆模式\n' +
      '• /發票 或 /記帳 ➜ 鎖定強制記帳模式\n' +
      '• 改存 聯絡人 ➜ 事後將上一筆轉移至聯絡人\n' +
      '• 改存 行事曆 ➜ 事後將上一筆轉移至行事曆\n' +
      '• /model ➜ 查詢與切換 Gemini AI 模型\n' +
      '• /model auto ➜ 恢復預設智慧動態輪詢\n' +
      '• /查 聯絡人 ➜ 查詢通訊錄與電話\n' +
      '• /查 進度 ➜ 查看各班最新教學進度\n' +
      '• /查 缺交 ➜ 查看未補交作業清單\n' +
      '• /查 今日 ➜ 查看今日聯絡簿與行程\n' +
      '• /查 花費 ➜ 查看本月累計支出\n' +
      '• /查 <關鍵字> ➜ 跨表快速搜尋紀錄\n' +
      '• /del <關鍵字> ➜ 精準搜尋並刪除指定紀錄\n' +
      '• /undo ➜ 撤回並刪除上一筆存入的資料\n' +
      '• /help ➜ 顯示本指令說明清單\n━━━━━━━━━━━━━━\n' +
      '✨ 隨時在 LINE 傳送文字、語音或名片/發票圖片開始使用!';
  }

  return null;
}

// 取得 LINE 訊息多媒體檔案 Blob
function getLineMessageContentBlob(messageId) {
  const url = 'https://api-data.line.me/v2/bot/message/' + messageId + '/content';
  const token = CONFIG.LINE_ACCESS_TOKEN;
  const options = {
    method: 'get',
    headers: { 'Authorization': 'Bearer ' + token },
    muteHttpExceptions: true
  };
  const response = UrlFetchApp.fetch(url, options);
  const code = response.getResponseCode();
  if (code !== 200) {
    throw new Error('LINE 伺服器回傳狀態碼 ' + code + ',請確認 LINE_ACCESS_TOKEN 是否正確!');
  }
  return response.getBlob();
}

// ==================== 4. Gemini AI 語意解析核心 ====================
function getCandidateModels(apiKey) {
  const manualModel = PropertiesService.getScriptProperties().getProperty('GEMINI_MODEL');
  
  const baseDefaults = [
    'gemini-2.5-flash',
    'gemini-2.0-flash',
    'gemini-1.5-flash-latest',
    'gemini-1.5-flash',
    'gemini-2.5-flash-lite',
    'gemini-2.0-flash-lite'
  ];

  if (manualModel) {
    return [manualModel, ...baseDefaults.filter(m => m !== manualModel)];
  }

  try {
    const listUrl = 'https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey;
    const response = UrlFetchApp.fetch(listUrl, { method: 'get', muteHttpExceptions: true });
    const json = JSON.parse(response.getContentText());

    if (json.models && json.models.length > 0) {
      const validModels = json.models
        .filter(m => m.supportedGenerationMethods && m.supportedGenerationMethods.includes('generateContent'))
        .map(m => m.name.replace(/^models\//, ''))
        .filter(name => !/(omni|embed|tts|stt|imagen|realtime)/i.test(name))
        .filter(name => /flash/i.test(name));

      if (validModels.length > 0) {
        validModels.sort((a, b) => b.localeCompare(a, undefined, { numeric: true, sensitivity: 'base' }));
        return [...new Set([...validModels, ...baseDefaults])];
      }
    }
  } catch (e) {
    console.warn('[動態探測] 查詢清單失敗,使用預設清單:', e);
  }

  return baseDefaults;
}

function getSystemPrompt() {
  const now = new Date();
  const currentDateTimeStr = Utilities.formatDate(now, 'Asia/Taipei', 'yyyy-MM-dd HH:mm (E)');

  return `
你是專業的「全能教育與生活智慧管家」,專門協助教師、導師與上班族自動分析並整理訊息(支援文字、名片/發票/海報照片辨識、語音聽寫)。
當前台灣時間為:${currentDateTimeStr}。

【🌟 圖片與多模態輸入的「核心優先級判定規則」】:
1. 📇 圖片為「名片 / 講師介紹卡 / 通訊錄截圖」時(畫面中包含姓名、電話、手機、Email、學校/公司、職稱):
   👉 【必須 100% 強制判定為「聯絡人」】!
   ⚠️ 嚴格禁止誤判為行事曆!即使名片上有年份、創立日期、營業時間、活動名稱或地址,只要主要主體是「名片 / 介紹人」,一律嚴格歸類為「聯絡人」!
2. 💰 圖片為「發票 / 收據 / 結帳單 / 繳費單」時(畫面中包含消費品項、總金額):
   👉 【必須判定為「記帳」】!
3. 📅 圖片為「研習海報 / 講座宣傳單 / 會議簡章 / 公文」時(畫面主體為公告某個特定活動的舉辦日期與時間):
   👉 【判定為「行事曆」】!
4. 📢 圖片為「黑白板筆記 / 聯絡簿手寫字」時:
   👉 【判定為「今日聯絡簿」或「備課靈感」】!

請將使用者的輸入準確歸類為下列十二個分類之一:
1. "聯絡人":記錄新增聯絡人通訊錄、交換的名片、教師/同仁/家長/廠商之姓名、電話、手機、Email、學校或任職單位與職稱。(例如:拍名片照片、或輸入「新增聯絡人:王大明,0912-345678,ming@gmail.com,大仁國中理化老師」)
2. "教學進度":記錄各班上課進度、章節單元、進度頁數、交代作業或給小老師/學生的交代事項。(例如:「205班上到第二章,P.19頁,已提醒該班小老師要收作業簿給老師」)
3. "今日聯絡簿":包含作業、攜帶物品、發放回條/通知單、導師叮嚀事項。(例如:「今日聯絡簿:國習P20-22、數作第5回、發下流感疫苗意願書週五前交、明天穿運動服帶跳繩」)
4. "家長聯繫":記錄與家長的電話、面談、LINE 溝通紀錄、交代事項與承諾。(例如:「下午跟小明媽媽通電話,反應小明作業拖延,已建議設定計時器,下週五再追蹤」)
5. "備課靈感":教學活動設計、探究題目、課堂提問、教案靈感、實驗點子。(例如:「備課靈感:國二浮力單元可以用沉浮子玩具探究氣體體積與浮力關係」)
6. "作業缺交":登記缺交作業的學生座號或姓名、原因與補交期限。(例如:「國語習作沒交:5號、12號、23號,12號說明天補交」)
7. "學生概況":學生的課堂狀況、常規紀律、同儕衝突、獎懲、受傷送保健室。(注意:若有同行或關聯學生,務必拆分出主要學生與關聯學生)
8. "記帳":花費、買東西、發票收據、收入、繳費等金錢相關。(例如:「午餐排骨便當 110 元」或辨識出的發票總金額與明細)
9. "行事曆":特定時間的行程、會議、代辦、研習活動、預約。(例如:「明天下午 2 點在 3 樓會議室開會」或研習海報上的活動時間與地點)
10. "生活雜事":日常備忘、非特定時間的雜項待辦。(例如:「週末記得買濾水器濾心」)
11. "好物分享":推薦或收藏的書籍、工具、軟體、網購商品連結。(例如:「推薦這款人體工學椅超好坐特價3990 https://...」)
12. "AI諮詢":使用者提問請求解答、備課出題或閒聊。(例如:「幫我想三個國中理化浮力的生活情境探究題」)

你必須嚴格輸出 JSON 物件,格式如下:
{
  "category": "聯絡人" | "教學進度" | "今日聯絡簿" | "家長聯繫" | "備課靈感" | "作業缺交" | "學生概況" | "記帳" | "行事曆" | "生活雜事" | "好物分享" | "AI諮詢",
  "summary": "一句話摘要,15字以內",
  "contact": {
    "name": "姓名(如 王大明)",
    "phone": "電話或手機號碼,若無填'無'",
    "email": "電子郵件,若無填'無'",
    "company_title": "單位、學校、任教科目或職稱(如 大仁國中 理化老師 / 教務主任)",
    "notes": "其他備註或介紹人,若無填'無'"
  },
  "teaching_progress": {
    "class_name": "班級名稱(如 205班、一年三班)",
    "subject": "科目或冊別(如 國文、理化、第二冊,若未提及填'未註明')",
    "progress": "章節單元與進度頁數(如 第二章 P.19)",
    "notes": "交代事項、作業繳交提醒或備註,若無填'無'"
  },
  "contact_book": {
    "homework": "條列今日作業,例如:1. 國習P20-22\\n2. 數作第5回",
    "bring_items": "條列攜帶物品,若無填'無'",
    "notices": "發放通知單/回條與繳交期限,若無填'無'",
    "teacher_words": "導師的話或生活提醒,若無填'無'"
  },
  "parent_log": {
    "student_name": "學生姓名/座號",
    "parent_role": "家長稱謂(如媽媽、爸爸、監護人)",
    "contact_type": "電話/面談/LINE/聯絡簿/其他",
    "content": "溝通主旨與詳細內容",
    "action_plan": "約定事項或後續追蹤承諾,若無填'無'"
  },
  "teaching_idea": {
    "target_grade_subject": "適用年級或科目(如國中理化、國小數學、全年級)",
    "topic": "主題單元名稱",
    "core_idea": "核心點子、教學提問或活動設計詳情",
    "materials_needed": "所需器材或教材備註,若無填'無'"
  },
  "homework_missing": {
    "assignment_name": "作業名稱(如國語習作、數學學習單)",
    "missing_students": "缺交座號或學生名單(如 5號, 12號, 23號)",
    "reason_deadline": "原因與補交期限說明,若無填'明日補交'",
    "status": "未補交/待追蹤"
  },
  "student": {
    "primary_student": "主要學生姓名/座號",
    "related_students": "關聯/同行/衝突學生姓名,若無填'無'",
    "event_type": "學習表現/常規紀律/同儕衝突/受傷處置/其他",
    "detail": "事件詳細內容與觀察處置",
    "note": "後續備註,若無填'無'"
  },
  "expense": {
    "type": "支出" | "收入",
    "item": "項目名稱",
    "amount": 100,
    "classification": "餐飲/交通/購物/文具教材/娛樂/其他",
    "note": "備註,無則填'無'"
  },
  "calendar": {
    "title": "事項名稱",
    "target_time": "推算出的具體日期時間(如 2026/08/24 14:00),無指定填'待定'",
    "location": "地點,無則填'無'",
    "note": "備註,無則填'無'"
  },
  "task": {
    "title": "雜事主旨",
    "detail": "詳細說明",
    "note": "備註,無則填'無'"
  },
  "goods": {
    "name": "好物/工具/書籍名稱",
    "url": "擷取出的網址連結,若無填'無'",
    "reason": "推薦原因或心得重點",
    "note": "價格或其他備註,無則填'無'"
  },
  "ai_reply": "若為 AI諮詢,請在此給出精準熱情且專業的回覆;若非 AI諮詢則留空即可"
}
`;
}

function executeGeminiGenerate(parts) {
  const apiKey = CONFIG.GEMINI_API_KEY;
  if (!apiKey || apiKey === 'YOUR_GEMINI_API_KEY') {
    return {
      category: 'AI諮詢',
      summary: '未設定 Gemini API 金鑰',
      ai_reply: '⚠️ 系統尚未設定 GEMINI_API_KEY,請在 Google 試算表「擴充功能 > Apps Script > 專案設定 > 指令碼屬性」填入您的 API 金鑰。'
    };
  }

  const candidateModels = getCandidateModels(apiKey);
  let lastErrorMessage = '';

  for (const modelName of candidateModels) {
    const url = 'https://generativelanguage.googleapis.com/v1beta/models/' + modelName + ':generateContent?key=' + apiKey;
    
    const payload = {
      contents: [
        {
          role: 'user',
          parts: parts
        }
      ],
      generationConfig: {
        responseMimeType: 'application/json',
        temperature: 0.2
      }
    };

    const options = {
      method: 'post',
      contentType: 'application/json',
      payload: JSON.stringify(payload),
      muteHttpExceptions: true
    };

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

      if (json.error) {
        lastErrorMessage = json.error.message;
        console.warn('[模型輪詢] ' + modelName + ' 失敗: ' + lastErrorMessage + ',立即切換下一個模型...');
        continue;
      }

      if (json.candidates && json.candidates.length > 0 && json.candidates[0].content && json.candidates[0].content.parts) {
        const candidateText = json.candidates[0].content.parts[0].text;
        console.log('[模型成功] 使用模型: ' + modelName);
        return JSON.parse(candidateText);
      }
    } catch (e) {
      lastErrorMessage = e.toString();
      console.warn('[模型輪詢] ' + modelName + ' 連線異常: ' + e + ',嘗試下一個...');
    }
  }

  return {
    category: 'AI諮詢',
    summary: 'AI 解析失敗',
    ai_reply: '⚠️ AI 解析發生錯誤(' + lastErrorMessage + '),請檢查 Gemini API Key 是否正確。'
  };
}

function parseWithGemini(userText) {
  const systemPrompt = getSystemPrompt();
  const parts = [
    { text: systemPrompt + '\n\n使用者輸入:\n' + userText }
  ];
  return executeGeminiGenerate(parts);
}

function parseWithGeminiMultimodal(mediaObj) {
  const systemPrompt = getSystemPrompt();
  let mediaNotice = '';
  if (mediaObj.type === 'image') {
    const forceNotice = mediaObj.forceCategory
      ? '\n\n【🔥 核心特別指令:使用者已明確強制指定此圖片必須歸類為「' + mediaObj.forceCategory + '」!請務必將 category 設為 "' + mediaObj.forceCategory + '" 並提取該分類對應的所有結構化欄位!】'
      : '';

    mediaNotice = '\n\n【重要圖片多模態辨識指示】:\n' +
      '1. 若圖片為【名片/商務卡/講師介紹】(畫面有人名、職稱、電話、Email、機構名):必須 100% 強制歸類為 "category": "聯絡人",請提取姓名 (name)、電話 (phone)、電子郵件 (email)、任職單位與職稱 (company_title)、備註 (notes)。絕不可誤判為行事曆!\n' +
      '2. 若圖片為【發票/收據】:必須歸類為 "category": "記帳" 並提取總金額。\n' +
      '3. 若圖片為【研習/活動海報】:必須歸類為 "category": "行事曆" 並提取時間地點。\n' +
      '4. 請仔細辨識圖片所有文字,輸出最精確的結構化 JSON 物件。' + forceNotice;
  } else {
    mediaNotice = '\n\n【注意:使用者上傳了一段語音訊息。請先將音訊內容完整聽寫為繁體中文,再進行語意分析與結構化 JSON 分類】';
  }

  const parts = [
    { text: systemPrompt + mediaNotice },
    {
      inlineData: {
        mimeType: mediaObj.mimeType,
        data: mediaObj.base64Data
      }
    }
  ];
  return executeGeminiGenerate(parts);
}

// ==================== 5. Google 試算表與 Google 聯絡人自動分類儲存 ====================
function saveToSpreadsheet(parsed, rawText) {
  const ssId = CONFIG.SPREADSHEET_ID;
  if (!ssId || ssId === 'YOUR_SPREADSHEET_ID') {
    return { success: false, message: '未設定 Google 試算表 ID' };
  }

  let ss;
  try {
    ss = SpreadsheetApp.openById(ssId);
  } catch (e) {
    return { success: false, message: '無法開啟試算表:' + e.message };
  }

  const now = new Date();
  const timestamp = Utilities.formatDate(now, 'Asia/Taipei', 'yyyy-MM-dd HH:mm:ss');
  const cat = parsed.category;

  if (cat === 'AI諮詢') {
    return { success: true, isAiQuery: true, sheetName: null };
  }

  ensureSheetHeaders(ss);

  let sheetName = '';
  let rowData = [];

  switch (cat) {
    case '聯絡人':
      sheetName = CONFIG.SHEET_NAMES.CONTACTS;
      const ct = parsed.contact || {};
      
      const contactSyncResult = createGoogleContact(ct);
      const contactStatus = contactSyncResult.success ? '✅ 已同步Google聯絡人' : '未同步';

      rowData = [
        timestamp,
        ct.name || parsed.summary || rawText,
        ct.phone || '無',
        ct.email || '無',
        ct.company_title || '未註明',
        ct.notes || '無',
        contactStatus
      ];
      parsed._contactSync = contactSyncResult.success;
      break;

    case '教學進度':
      sheetName = CONFIG.SHEET_NAMES.TEACHING_PROGRESS;
      const tp = parsed.teaching_progress || {};
      rowData = [
        timestamp,
        tp.class_name || '未指定班級',
        tp.subject || '未指定',
        tp.progress || parsed.summary || rawText,
        tp.notes || '無'
      ];
      break;

    case '今日聯絡簿':
      sheetName = CONFIG.SHEET_NAMES.CONTACT_BOOK;
      const cb = parsed.contact_book || {};
      rowData = [
        timestamp,
        cb.homework || '無',
        cb.bring_items || '無',
        cb.notices || '無',
        cb.teacher_words || '無'
      ];
      break;

    case '家長聯繫':
      sheetName = CONFIG.SHEET_NAMES.PARENT_LOG;
      const pl = parsed.parent_log || {};
      rowData = [
        timestamp,
        pl.student_name || parsed.summary || rawText,
        pl.parent_role || '家長',
        pl.contact_type || '電話/面談',
        pl.content || rawText,
        pl.action_plan || '無'
      ];
      break;

    case '備課靈感':
      sheetName = CONFIG.SHEET_NAMES.TEACHING_IDEA;
      const ti = parsed.teaching_idea || {};
      rowData = [
        timestamp,
        ti.target_grade_subject || '未註明',
        ti.topic || parsed.summary || '未指定主題',
        ti.core_idea || rawText,
        ti.materials_needed || '無'
      ];
      break;

    case '作業缺交':
      sheetName = CONFIG.SHEET_NAMES.HOMEWORK_MISSING;
      const hm = parsed.homework_missing || {};
      rowData = [
        timestamp,
        hm.assignment_name || parsed.summary || rawText,
        hm.missing_students || '未指定',
        hm.reason_deadline || '明日補交',
        hm.status || '未補交'
      ];
      break;

    case '學生概況':
      sheetName = CONFIG.SHEET_NAMES.STUDENT;
      const stu = parsed.student || {};
      rowData = [
        timestamp,
        stu.primary_student || parsed.summary || rawText,
        stu.related_students || '無',
        stu.event_type || '一般紀錄',
        stu.detail || rawText,
        stu.note || '無'
      ];
      break;

    case '記帳':
      sheetName = CONFIG.SHEET_NAMES.EXPENSE;
      const exp = parsed.expense || {};
      rowData = [
        timestamp,
        exp.type || '支出',
        exp.classification || '一般',
        exp.item || parsed.summary || rawText,
        Number(exp.amount) || 0,
        exp.note || '無'
      ];
      break;

    case '行事曆':
      sheetName = CONFIG.SHEET_NAMES.CALENDAR;
      const cal = parsed.calendar || {};
      
      const calResult = createGoogleCalendarEvent(cal);
      const calStatus = calResult.success ? '已同步Google日曆' : '未完成';

      rowData = [
        timestamp,
        cal.title || parsed.summary,
        cal.target_time || '待定',
        cal.location || '無',
        cal.note || '無',
        calStatus
      ];
      parsed._calSync = calResult.success;
      break;

    case '生活雜事':
      sheetName = CONFIG.SHEET_NAMES.TASK;
      const tsk = parsed.task || {};
      rowData = [
        timestamp,
        tsk.title || parsed.summary,
        tsk.detail || rawText,
        tsk.note || '無',
        '待辦'
      ];
      break;

    case '好物分享':
      sheetName = CONFIG.SHEET_NAMES.GOODS;
      const gds = parsed.goods || {};
      rowData = [
        timestamp,
        gds.name || parsed.summary,
        gds.url || '無',
        gds.reason || rawText,
        gds.note || '無'
      ];
      break;

    default:
      return { success: false, message: '未知分類: ' + cat };
  }

  const targetSheet = ss.getSheetByName(sheetName);
  if (targetSheet) {
    targetSheet.appendRow(rowData);
    const lastRow = targetSheet.getLastRow();

    PropertiesService.getScriptProperties().setProperty('LAST_SAVED_RECORD', JSON.stringify({
      sheetName: sheetName,
      row: lastRow,
      summary: parsed.summary || (rowData[1] ? String(rowData[1]) : '')
    }));

    return { success: true, sheetName: sheetName, rowData: rowData };
  } else {
    return { success: false, message: '找不到分頁: ' + sheetName };
  }
}

// 建立並美化 11 大分頁與表頭樣式
function ensureSheetHeaders(ss) {
  const schema = [
    {
      name: CONFIG.SHEET_NAMES.CONTACTS,
      color: '#303f9f',
      headers: ['記錄時間', '姓名', '電話/手機', 'Email', '單位/學校/職稱', '備註說明', 'Google聯絡人同步狀態']
    },
    {
      name: CONFIG.SHEET_NAMES.TEACHING_PROGRESS,
      color: '#00838f',
      headers: ['記錄時間', '班級', '科目/冊別', '章節單元與進度頁數', '交代事項與備註']
    },
    {
      name: CONFIG.SHEET_NAMES.CONTACT_BOOK,
      color: '#00695c',
      headers: ['記錄時間', '今日作業', '攜帶物品', '發放通知單/回條(繳交日)', '導師叮嚀與備註']
    },
    {
      name: CONFIG.SHEET_NAMES.PARENT_LOG,
      color: '#c2185b',
      headers: ['記錄時間', '學生姓名/座號', '家長稱謂', '溝通管道', '溝通主旨與詳細內容', '約定追蹤事項']
    },
    {
      name: CONFIG.SHEET_NAMES.TEACHING_IDEA,
      color: '#2e7d32',
      headers: ['記錄時間', '適用年級/科目', '主題單元', '核心教學點子/活動提問設計', '所需器材/備註']
    },
    {
      name: CONFIG.SHEET_NAMES.HOMEWORK_MISSING,
      color: '#d84315',
      headers: ['記錄時間', '作業名稱', '缺交名單/座號', '原因與補交期限', '追蹤狀態']
    },
    {
      name: CONFIG.SHEET_NAMES.STUDENT,
      color: '#b71c1c',
      headers: ['記錄時間', '主要學生', '關聯學生(同行/衝突)', '事件類型', '詳細觀察與處置', '後續備註']
    },
    {
      name: CONFIG.SHEET_NAMES.EXPENSE,
      color: '#1b5e20',
      headers: ['記錄時間', '收支類型', '分類', '項目名稱', '金額(元)', '備註']
    },
    {
      name: CONFIG.SHEET_NAMES.CALENDAR,
      color: '#0d47a1',
      headers: ['記錄時間', '事項名稱', '預定日期時間', '地點', '備註', '完成狀態']
    },
    {
      name: CONFIG.SHEET_NAMES.TASK,
      color: '#e65100',
      headers: ['記錄時間', '事項主旨', '詳細內容說明', '備註', '處理狀態']
    },
    {
      name: CONFIG.SHEET_NAMES.GOODS,
      color: '#4a148c',
      headers: ['記錄時間', '好物/書籍/工具名稱', '網址連結', '推薦心得與特色', '價格/備註']
    }
  ];

  schema.forEach(item => {
    let sheet = ss.getSheetByName(item.name);
    if (!sheet) {
      sheet = ss.insertSheet(item.name);
      sheet.appendRow(item.headers);
      
      const headerRange = sheet.getRange(1, 1, 1, item.headers.length);
      headerRange.setBackground(item.color)
                 .setFontColor('#ffffff')
                 .setFontWeight('bold')
                 .setHorizontalAlignment('center');
      sheet.setFrozenRows(1);
      sheet.setRowHeight(1, 35);
      
      for (let i = 1; i <= item.headers.length; i++) {
        sheet.setColumnWidth(i, 165);
      }
    }
  });
}

// ==================== 6. 格式化 LINE 回覆訊息 ====================
function formatReplyMessage(parsed, writeResult) {
  const cat = parsed.category;

  if (cat === 'AI諮詢') {
    return parsed.ai_reply || '管家收到您的訊息囉!';
  }

  if (!writeResult.success) {
    return '⚠️ 記錄失敗:' + writeResult.message + '\n(請檢查試算表 ID 與授權)';
  }

  if (cat === '今日聯絡簿') {
    const cb = parsed.contact_book || {};
    const dateStr = Utilities.formatDate(new Date(), 'Asia/Taipei', 'yyyy/MM/dd (E)');
    return '📢 【' + dateStr + ' 班級聯絡簿公告】\n(已存入試算表,可直接長按複製轉發班群👇)\n━━━━━━━━━━━━━━\n📚 【今日作業】\n' + (cb.homework || '無') + '\n\n🎒 【攜帶物品】\n' + (cb.bring_items || '無') + '\n\n📄 【重要通知 / 回條】\n' + (cb.notices || '無') + '\n\n❤️ 【導師叮嚀】\n' + (cb.teacher_words || '請家長撥冗簽閱聯絡簿,祝大家有美好的一天!') + '\n━━━━━━━━━━━━━━\n✨ 試算表【今日聯絡簿】已同步歸檔!';
  }

  let icon = '📝';
  let detailText = '';

  switch (cat) {
    case '聯絡人':
      icon = '👤';
      const ct = parsed.contact || {};
      const contactSyncText = parsed._contactSync ? '(✅ 已自動新增至 Google 聯絡人)' : '';
      detailText = '• 姓名:' + (ct.name || '未註明') + ' ' + contactSyncText + '\n• 電話:' + (ct.phone || '無') + '\n• Email:' + (ct.email || '無') + '\n• 單位/職稱:' + (ct.company_title || '無') + '\n• 備註:' + (ct.notes || '無');
      break;

    case '教學進度':
      icon = '📖';
      const tp = parsed.teaching_progress || {};
      detailText = '• 班級:' + (tp.class_name || '未指定') + '\n• 科目/冊別:' + (tp.subject || '未指定') + '\n• 章節進度:' + (tp.progress || '無') + '\n• 交代事項:' + (tp.notes || '無');
      break;

    case '家長聯繫':
      icon = '📞';
      const pl = parsed.parent_log || {};
      detailText = '• 學生:' + (pl.student_name || '未指定') + '(' + (pl.parent_role || '家長') + ')\n• 管道:' + (pl.contact_type || '電話') + '\n• 內容:' + (pl.content || '無') + '\n• 約定追蹤:' + (pl.action_plan || '無');
      break;

    case '備課靈感':
      icon = '💡';
      const ti = parsed.teaching_idea || {};
      detailText = '• 科目/年級:' + (ti.target_grade_subject || '未註明') + '\n• 主題:' + (ti.topic || '未指定') + '\n• 教學點子:' + (ti.core_idea || '無') + '\n• 所需器材:' + (ti.materials_needed || '無');
      break;

    case '作業缺交':
      icon = '📝';
      const hm = parsed.homework_missing || {};
      detailText = '• 作業:' + (hm.assignment_name || '未指定') + '\n• 缺交名單:' + (hm.missing_students || '無') + '\n• 原因/期限:' + (hm.reason_deadline || '無') + '\n• 狀態:' + (hm.status || '未補交');
      break;

    case '學生概況':
      icon = '👨‍🎓';
      const stu = parsed.student || {};
      detailText = '• 主要學生:' + (stu.primary_student || '未指定') + '\n• 關聯學生:' + (stu.related_students || '無') + '\n• 類別:' + (stu.event_type || '一般') + '\n• 處置紀錄:' + (stu.detail || '無') + '\n• 後續:' + (stu.note || '無');
      break;

    case '記帳':
      icon = '💰';
      const exp = parsed.expense || {};
      detailText = '• 類型:' + (exp.type || '支出') + '(' + (exp.classification || '一般') + ')\n• 項目:' + (exp.item || '未註明') + '\n• 金額:$' + (exp.amount || 0) + ' 元\n• 備註:' + (exp.note || '無');
      break;

    case '行事曆':
      icon = '📅';
      const cal = parsed.calendar || {};
      const syncText = parsed._calSync ? '(✅ 已同步建立至 Google 日曆)' : '';
      detailText = '• 事項:' + (cal.title || '未命名') + '\n• 時間:' + (cal.target_time || '待定') + ' ' + syncText + '\n• 地點:' + (cal.location || '無') + '\n• 備註:' + (cal.note || '無');
      break;

    case '生活雜事':
      icon = '📌';
      const tsk = parsed.task || {};
      detailText = '• 事項:' + (tsk.title || '未命名') + '\n• 內容:' + (tsk.detail || '無') + '\n• 備註:' + (tsk.note || '無');
      break;

    case '好物分享':
      icon = '🎁';
      const gds = parsed.goods || {};
      detailText = '• 名稱:' + (gds.name || '未命名') + '\n• 連結:' + (gds.url || '無') + '\n• 心得:' + (gds.reason || '無');
      break;
  }

  return icon + ' 【已為您自動歸檔至「' + writeResult.sheetName + '」】\n━━━━━━━━━━━━━━\n' + detailText + '\n━━━━━━━━━━━━━━\n✨ 試算表已同步更新完成!\n(💡 輸入 /undo 撤回上一筆,或 /del <關鍵字> 刪除特定紀錄)';
}

// ==================== 7. LINE API 發送核心 (Reply & Push) ====================
function sendLineReply(replyToken, text) {
  const url = 'https://api.line.me/v2/bot/message/reply';
  const token = CONFIG.LINE_ACCESS_TOKEN;

  if (!token || token === 'YOUR_LINE_ACCESS_TOKEN') {
    console.error('LINE Access Token 未設定');
    return;
  }

  const payload = {
    replyToken: replyToken,
    messages: [{ type: 'text', text: text }]
  };

  const options = {
    method: 'post',
    contentType: 'application/json',
    headers: { 'Authorization': 'Bearer ' + token },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };

  try {
    UrlFetchApp.fetch(url, options);
  } catch (e) {
    console.error('LINE Reply 發送失敗:', e);
  }
}

function sendLinePush(userId, text) {
  const url = 'https://api.line.me/v2/bot/message/push';
  const token = CONFIG.LINE_ACCESS_TOKEN;

  if (!token || token === 'YOUR_LINE_ACCESS_TOKEN' || !userId) {
    console.error('LINE Push 未設定 token 或無目標 userId');
    return;
  }

  const payload = {
    to: userId,
    messages: [{ type: 'text', text: text }]
  };

  const options = {
    method: 'post',
    contentType: 'application/json',
    headers: { 'Authorization': 'Bearer ' + token },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };

  try {
    UrlFetchApp.fetch(url, options);
    console.log('[LINE Push 成功] 推播給: ' + userId);
  } catch (e) {
    console.error('LINE Push 發送失敗:', e);
  }
}

// ==================== 8. 每日早安晨報與定時觸發器 ====================
function sendDailyMorningReport() {
  const userId = PropertiesService.getScriptProperties().getProperty('ADMIN_USER_ID');
  if (!userId) {
    console.warn('尚未記錄 ADMIN_USER_ID,請先在 LINE 與管家傳送任意訊息以完成綁定。');
    return;
  }

  const ss = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID);
  const now = new Date();
  const dateStr = Utilities.formatDate(now, 'Asia/Taipei', 'yyyy/MM/dd (E)');

  let calendarBrief = '無特定行程';
  try {
    const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0);
    const todayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59);
    const events = CalendarApp.getDefaultCalendar().getEvents(todayStart, todayEnd);
    if (events && events.length > 0) {
      calendarBrief = events.map(ev => '• ' + ev.getTitle() + ' (' + Utilities.formatDate(ev.getStartTime(), 'Asia/Taipei', 'HH:mm') + ')').join('\n');
    }
  } catch (calErr) {
    console.warn('讀取 Google 日曆失敗:', calErr);
  }

  let missingBrief = '無缺交,全員到齊 🎉';
  try {
    const missingSheet = ss.getSheetByName(CONFIG.SHEET_NAMES.HOMEWORK_MISSING);
    if (missingSheet && missingSheet.getLastRow() > 1) {
      const data = missingSheet.getDataRange().getValues();
      const unsubmitted = [];
      for (let i = 1; i < data.length; i++) {
        if (data[i][4] === '未補交') {
          unsubmitted.push('• ' + data[i][1] + ':' + data[i][2]);
        }
      }
      if (unsubmitted.length > 0) {
        missingBrief = unsubmitted.slice(0, 5).join('\n');
      }
    }
  } catch (mErr) {}

  let taskBrief = '無待辦事項';
  try {
    const taskSheet = ss.getSheetByName(CONFIG.SHEET_NAMES.TASK);
    if (taskSheet && taskSheet.getLastRow() > 1) {
      const data = taskSheet.getDataRange().getValues();
      const todos = [];
      for (let i = 1; i < data.length; i++) {
        if (data[i][4] === '待辦') {
          todos.push('• ' + data[i][1]);
        }
      }
      if (todos.length > 0) {
        taskBrief = todos.slice(0, 5).join('\n');
      }
    }
  } catch (tErr) {}

  const morningMessage = '🌅 【' + dateStr + ' 早安!全能管家今日晨報】\n━━━━━━━━━━━━━━\n' +
    '📅 【今日行程與會議】\n' + calendarBrief + '\n\n' +
    '📝 【待追蹤作業缺交】\n' + missingBrief + '\n\n' +
    '📌 【待處理生活備忘】\n' + taskBrief + '\n━━━━━━━━━━━━━━\n' +
    '✨ 祝老師今天教學順利、心情愉快!💪';

  sendLinePush(userId, morningMessage);
}

function setupDailyTrigger() {
  removeDailyTrigger();
  ScriptApp.newTrigger('sendDailyMorningReport')
    .timeBased()
    .atHour(7)
    .nearMinute(30)
    .everyDays(1)
    .inTimezone('Asia/Taipei')
    .create();
  Logger.log('✅ 每日早安晨報定時觸發器已建立(每天早上 07:30 推播)!');
}

function removeDailyTrigger() {
  const triggers = ScriptApp.getProjectTriggers();
  triggers.forEach(t => {
    if (t.getHandlerFunction() === 'sendDailyMorningReport') {
      ScriptApp.deleteTrigger(t);
    }
  });
  Logger.log('🧹 已清除每日早安晨報定時觸發器。');
}

// ==================== 9. Google 日曆與 Google 聯絡人自動同步核心 ====================
function createGoogleCalendarEvent(cal) {
  if (!cal || !cal.title) return { success: false, error: '無事項標題' };

  try {
    const calendar = CalendarApp.getDefaultCalendar();
    const targetTimeStr = cal.target_time;
    const title = cal.title;
    const location = (cal.location && cal.location !== '無') ? cal.location : '';
    const note = (cal.note && cal.note !== '無') ? cal.note : '無';
    const description = '【LINE 全能教師管家自動建立】\n• 事項:' + title + '\n• 地點:' + (location || '無') + '\n• 備註:' + note;

    if (targetTimeStr && targetTimeStr !== '待定') {
      const parsedDate = new Date(targetTimeStr.replace(/-/g, '/'));
      
      if (!isNaN(parsedDate.getTime())) {
        if (targetTimeStr.includes(':')) {
          const endTime = new Date(parsedDate.getTime() + 60 * 60 * 1000);
          calendar.createEvent(title, parsedDate, endTime, {
            location: location,
            description: description
          });
          return { success: true, isAllDay: false };
        } else {
          calendar.createAllDayEvent(title, parsedDate, {
            location: location,
            description: description
          });
          return { success: true, isAllDay: true };
        }
      }
    }

    const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000);
    calendar.createAllDayEvent('[待定] ' + title, tomorrow, {
      location: location,
      description: description + '\n(原始推算時間:' + targetTimeStr + ')'
    });
    return { success: true, isAllDay: true };
  } catch (e) {
    console.error('Google 日曆同步異常:', e);
    return { success: false, error: e.toString() };
  }
}

function createGoogleContact(ct) {
  if (!ct || !ct.name || ct.name === '未註明') return { success: false, error: '無姓名' };

  try {
    const fullName = String(ct.name).trim();
    let familyName = '';
    let givenName = fullName;

    if (fullName.length === 2) {
      familyName = fullName.substring(0, 1);
      givenName = fullName.substring(1);
    } else if (fullName.length === 3) {
      familyName = fullName.substring(0, 1);
      givenName = fullName.substring(1);
    } else if (fullName.length === 4) {
      familyName = fullName.substring(0, 2);
      givenName = fullName.substring(2);
    }

    const email = (ct.email && ct.email !== '無' && ct.email.includes('@')) ? ct.email.trim() : '';
    const phone = (ct.phone && ct.phone !== '無') ? String(ct.phone).trim() : '';
    const companyTitle = (ct.company_title && ct.company_title !== '未註明') ? ct.company_title.trim() : '';
    const notes = (ct.notes && ct.notes !== '無') ? ct.notes.trim() : '';
    const noteContent = '【LINE 全能智慧管家自動建立】\n• 單位/職稱:' + companyTitle + '\n• 備註:' + notes;

    const personPayload = {
      names: [
        {
          familyName: familyName,
          givenName: givenName,
          displayName: fullName
        }
      ]
    };

    if (phone) {
      personPayload.phoneNumbers = [{ value: phone, type: 'mobile' }];
    }

    if (email) {
      personPayload.emailAddresses = [{ value: email, type: 'work' }];
    }

    if (companyTitle) {
      personPayload.organizations = [{ name: companyTitle, title: '教師/同仁/教育夥伴' }];
    }

    if (noteContent) {
      personPayload.biographies = [{ value: noteContent, contentType: 'TEXT_PLAIN' }];
    }

    if (typeof People !== 'undefined' && People.People && People.People.createContact) {
      const resp = People.People.createContact(personPayload);
      console.log('[Google 聯絡人建立成功 (Advanced Service)] ' + fullName + ' (' + phone + ')');
      return { success: true, resourceName: resp.resourceName };
    }

    const token = ScriptApp.getOAuthToken();
    const url = 'https://people.googleapis.com/v1/people:createContact';
    const options = {
      method: 'post',
      contentType: 'application/json',
      headers: { 'Authorization': 'Bearer ' + token },
      payload: JSON.stringify(personPayload),
      muteHttpExceptions: true
    };

    const response = UrlFetchApp.fetch(url, options);
    const respCode = response.getResponseCode();
    const respText = response.getContentText();

    if (respCode === 200 || respCode === 201) {
      const respJson = JSON.parse(respText);
      console.log('[Google 聯絡人建立成功 (REST)] ' + fullName + ' (' + phone + ')');
      return { success: true, resourceName: respJson.resourceName };
    } else {
      console.warn('[Google 聯絡人建立失敗] HTTP ' + respCode + ': ' + respText);
      return { success: false, error: 'People API 回傳 (' + respCode + '): ' + respText };
    }
  } catch (e) {
    console.error('Google 聯絡人同步異常:', e);
    return { success: false, error: e.toString() };
  }
}

// ==================== 10. 個人雲端儀表板 HTML 渲染與 google.script.run 原生核心 ====================
function renderDashboardHtml() {
  return "<!DOCTYPE html>\n<html lang=\"zh-TW\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>全能教師智慧管家・個人雲端儀表板</title>\n  <link href=\"https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&family=Noto+Sans+TC:wght@400;500;700&display=swap\" rel=\"stylesheet\">\n  <style>\n    :root {\n      --bg-primary: #F8FAFC;\n      --bg-card: #FFFFFF;\n      --text-main: #1E293B;\n      --text-muted: #64748B;\n      --accent-orange: #F97316;\n      --accent-orange-light: #FFF7ED;\n      --accent-green: #10B981;\n      --accent-blue: #0284C7;\n      --accent-purple: #6366F1;\n      --border-color: #E2E8F0;\n      --card-shadow: 0 4px 20px -2px rgba(0, 0, 0, 0.05);\n    }\n    * { box-sizing: border-box; margin: 0; padding: 0; }\n    body {\n      font-family: 'Plus Jakarta Sans', 'Noto Sans TC', sans-serif;\n      background-color: var(--bg-primary);\n      color: var(--text-main);\n      padding: 16px;\n      max-width: 1240px;\n      margin: 0 auto;\n      min-height: 100vh;\n    }\n    header {\n      display: flex;\n      justify-content: space-between;\n      align-items: center;\n      padding: 16px 20px;\n      background: var(--bg-card);\n      border-radius: 16px;\n      box-shadow: var(--card-shadow);\n      margin-bottom: 20px;\n      flex-wrap: wrap;\n      gap: 12px;\n    }\n    .brand-title { display: flex; align-items: center; gap: 12px; }\n    .brand-icon {\n      width: 44px;\n      height: 44px;\n      background: var(--accent-orange-light);\n      color: var(--accent-orange);\n      border-radius: 12px;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      font-size: 24px;\n    }\n    .brand-text h1 { font-size: 1.25rem; font-weight: 800; color: var(--text-main); }\n    .brand-text p { font-size: 0.85rem; color: var(--text-muted); }\n    .header-actions { display: flex; gap: 10px; }\n    .btn {\n      padding: 10px 16px;\n      border-radius: 10px;\n      font-weight: 600;\n      cursor: pointer;\n      display: inline-flex;\n      align-items: center;\n      gap: 6px;\n      border: none;\n      font-size: 0.9rem;\n      transition: all 0.2s;\n    }\n    .btn-refresh { background: #F1F5F9; color: #475569; }\n    .btn-refresh:hover { background: #E2E8F0; }\n    .btn-add { background: var(--accent-orange); color: white; }\n    .btn-add:hover { opacity: 0.9; box-shadow: 0 4px 12px rgba(249,115,22,0.3); }\n    .stats-grid {\n      display: grid;\n      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n      gap: 16px;\n      margin-bottom: 24px;\n    }\n    .stat-card {\n      background: var(--bg-card);\n      padding: 20px;\n      border-radius: 16px;\n      box-shadow: var(--card-shadow);\n      border-left: 4px solid var(--accent-purple);\n    }\n    .stat-card.orange { border-left-color: var(--accent-orange); }\n    .stat-card.blue { border-left-color: var(--accent-blue); }\n    .stat-card.green { border-left-color: var(--accent-green); }\n    .stat-label { font-size: 0.85rem; color: var(--text-muted); margin-bottom: 6px; }\n    .stat-value { font-size: 1.5rem; font-weight: 800; color: var(--text-main); }\n    .tabs-wrapper {\n      display: flex;\n      gap: 8px;\n      overflow-x: auto;\n      padding-bottom: 12px;\n      margin-bottom: 16px;\n      scrollbar-width: thin;\n    }\n    .tab-btn {\n      background: var(--bg-card);\n      border: 1px solid var(--border-color);\n      padding: 10px 18px;\n      border-radius: 12px;\n      font-size: 0.9rem;\n      font-weight: 600;\n      color: var(--text-muted);\n      cursor: pointer;\n      white-space: nowrap;\n      transition: all 0.2s;\n    }\n    .tab-btn.active {\n      background: var(--accent-orange);\n      color: white;\n      border-color: var(--accent-orange);\n      box-shadow: 0 4px 12px rgba(249, 115, 22, 0.25);\n    }\n    .content-card {\n      background: var(--bg-card);\n      border-radius: 16px;\n      box-shadow: var(--card-shadow);\n      padding: 20px;\n      overflow: hidden;\n    }\n    .card-header {\n      display: flex;\n      justify-content: space-between;\n      align-items: center;\n      margin-bottom: 16px;\n      flex-wrap: wrap;\n      gap: 10px;\n    }\n    .card-header-left { display: flex; align-items: center; gap: 10px; }\n    .card-header h2 { font-size: 1.15rem; font-weight: 700; }\n    .badge-count {\n      background: var(--accent-orange-light);\n      color: var(--accent-orange);\n      padding: 4px 10px;\n      border-radius: 20px;\n      font-size: 0.8rem;\n      font-weight: 700;\n    }\n    .table-container {\n      overflow-x: auto;\n      max-height: 600px;\n    }\n    .data-table {\n      width: 100%;\n      border-collapse: collapse;\n      text-align: left;\n      font-size: 0.9rem;\n    }\n    .data-table th {\n      background: #F8FAFC;\n      padding: 12px 14px;\n      font-weight: 700;\n      color: var(--text-muted);\n      border-bottom: 2px solid var(--border-color);\n      position: sticky;\n      top: 0;\n      z-index: 10;\n      white-space: nowrap;\n    }\n    .data-table td {\n      padding: 12px 14px;\n      border-bottom: 1px solid var(--border-color);\n      vertical-align: middle;\n    }\n    .data-table tr:hover { background-color: #FAFAFA; }\n    .badge-tag {\n      display: inline-flex;\n      align-items: center;\n      gap: 4px;\n      padding: 5px 10px;\n      border-radius: 8px;\n      font-size: 0.82rem;\n      font-weight: 700;\n      cursor: pointer;\n      user-select: none;\n      transition: all 0.2s;\n      border: 1px solid transparent;\n    }\n    .badge-tag:hover { transform: scale(1.04); }\n    .badge-red { background: #FEE2E2; color: #B91C1C; border-color: #FECACA; }\n    .badge-orange { background: #FFEDD5; color: #C2410C; border-color: #FED7AA; }\n    .badge-green { background: #D1FAE5; color: #047857; border-color: #A7F3D0; }\n    .badge-blue { background: #E0F2FE; color: #0369A1; border-color: #BAE6FD; }\n    .action-btn {\n      padding: 5px 8px;\n      border-radius: 6px;\n      border: 1px solid #CBD5E1;\n      background: white;\n      color: #475569;\n      cursor: pointer;\n      font-size: 0.8rem;\n      font-weight: 600;\n      transition: all 0.15s;\n      margin-right: 4px;\n    }\n    .action-btn:hover { background: #F1F5F9; color: #0F172A; }\n    .action-btn.btn-del:hover { background: #FEE2E2; color: #DC2626; border-color: #FCA5A5; }\n    .empty-state {\n      text-align: center;\n      padding: 40px 20px;\n      color: var(--text-muted);\n    }\n    .modal-backdrop {\n      position: fixed;\n      top: 0; left: 0; right: 0; bottom: 0;\n      background: rgba(15, 23, 42, 0.5);\n      backdrop-filter: blur(4px);\n      display: none;\n      align-items: center;\n      justify-content: center;\n      z-index: 1000;\n      padding: 16px;\n    }\n    .modal-box {\n      background: white;\n      border-radius: 16px;\n      width: 100%;\n      max-width: 520px;\n      padding: 24px;\n      box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);\n      max-height: 90vh;\n      overflow-y: auto;\n    }\n    .modal-header {\n      display: flex;\n      justify-content: space-between;\n      align-items: center;\n      margin-bottom: 16px;\n      padding-bottom: 12px;\n      border-bottom: 1px solid var(--border-color);\n    }\n    .modal-header h3 { font-size: 1.15rem; font-weight: 700; }\n    .modal-close { background: none; border: none; font-size: 20px; cursor: pointer; color: var(--text-muted); }\n    .form-group {\n      margin-bottom: 14px;\n    }\n    .form-group label {\n      display: block;\n      font-size: 0.85rem;\n      font-weight: 700;\n      color: var(--text-main);\n      margin-bottom: 6px;\n    }\n    .form-control {\n      width: 100%;\n      padding: 10px 14px;\n      border: 1px solid #CBD5E1;\n      border-radius: 10px;\n      font-size: 0.95rem;\n      font-family: inherit;\n      outline: none;\n    }\n    .form-control:focus { border-color: var(--accent-orange); box-shadow: 0 0 0 3px rgba(249,115,22,0.15); }\n    .modal-footer {\n      display: flex;\n      justify-content: flex-end;\n      gap: 10px;\n      margin-top: 20px;\n      padding-top: 14px;\n      border-top: 1px solid var(--border-color);\n    }\n    .toast-msg {\n      position: fixed;\n      bottom: 24px;\n      right: 24px;\n      background: #1E293B;\n      color: white;\n      padding: 12px 20px;\n      border-radius: 10px;\n      font-size: 0.9rem;\n      font-weight: 600;\n      box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2);\n      display: none;\n      z-index: 2000;\n      animation: fadeIn 0.3s ease;\n    }\n    @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }\n  </style>\n</head>\n<body>\n  <header>\n    <div class=\"brand-title\">\n      <div class=\"brand-icon\">🤖</div>\n      <div class=\"brand-text\">\n        <h1>全能智慧管家・個人雲端儀表板</h1>\n        <p>即時同步 Google 試算表 11 大專屬分頁 · 支援一鍵修改狀態與手動增刪編輯</p>\n      </div>\n    </div>\n    <div class=\"header-actions\">\n      <button class=\"btn btn-refresh\" onclick=\"loadAllData()\">🔄 重新整理</button>\n    </div>\n  </header>\n  <div class=\"stats-grid\">\n    <div class=\"stat-card\"><div class=\"stat-label\">👤 累計聯絡人人數</div><div class=\"stat-value\" id=\"statContacts\">載入中...</div></div>\n    <div class=\"stat-card orange\"><div class=\"stat-label\">💰 本期記帳總支出</div><div class=\"stat-value\" id=\"statExpense\">載入中...</div></div>\n    <div class=\"stat-card blue\"><div class=\"stat-label\">📝 待追蹤作業缺交</div><div class=\"stat-value\" id=\"statMissing\">載入中...</div></div>\n    <div class=\"stat-card green\"><div class=\"stat-label\">📢 最新聯絡簿發布</div><div class=\"stat-value\" id=\"statContact\" style=\"font-size: 1.05rem;\">載入中...</div></div>\n  </div>\n  <div class=\"tabs-wrapper\" id=\"tabsList\"></div>\n  <div class=\"content-card\">\n    <div class=\"card-header\">\n      <div class=\"card-header-left\">\n        <h2 id=\"currentTabTitle\">生活雜事</h2>\n        <span class=\"badge-count\" id=\"currentTabCount\">載入中...</span>\n      </div>\n      <button class=\"btn btn-add\" onclick=\"openAddModal()\">➕ 新增紀錄</button>\n    </div>\n    <div class=\"table-container\" id=\"tableContent\"><div class=\"empty-state\">⏳ 正在連線讀取 Google 試算表最新紀錄...</div></div>\n  </div>\n\n  <!-- 新增 / 編輯 Modal -->\n  <div class=\"modal-backdrop\" id=\"editModal\" onclick=\"if(event.target===this) closeModal()\">\n    <div class=\"modal-box\">\n      <div class=\"modal-header\">\n        <h3 id=\"modalTitle\">新增紀錄</h3>\n        <button class=\"modal-close\" onclick=\"closeModal()\">&times;</button>\n      </div>\n      <form id=\"editForm\" onsubmit=\"handleFormSubmit(event)\">\n        <div id=\"formFields\"></div>\n        <div class=\"modal-footer\">\n          <button type=\"button\" class=\"btn btn-refresh\" onclick=\"closeModal()\">取消</button>\n          <button type=\"submit\" class=\"btn btn-add\" id=\"btnSaveModal\">確認儲存</button>\n        </div>\n      </form>\n    </div>\n  </div>\n\n  <div class=\"toast-msg\" id=\"toast\"></div>\n\n  <script>\n    var APP_DATA = { sheets: {}, stats: { totalExpense: 0, missingCount: 0, todayContact: null, contactCount: 0 } };\n    var TAB_CONFIG = [\n      { name: '生活雜事', icon: '📌' },\n      { name: '作業缺交', icon: '📝' },\n      { name: '行事曆', icon: '📅' },\n      { name: '聯絡人', icon: '👤' },\n      { name: '教學進度', icon: '📖' },\n      { name: '今日聯絡簿', icon: '📢' },\n      { name: '學生概況', icon: '👨‍🎓' },\n      { name: '家長聯繫', icon: '📞' },\n      { name: '備課靈感', icon: '💡' },\n      { name: '記帳本', icon: '💰' },\n      { name: '好物分享', icon: '🎁' }\n    ];\n    var currentSheetName = '生活雜事';\n    var editingActualRow = null;\n\n    function showToast(msg) {\n      var t = document.getElementById('toast');\n      if (t) {\n        t.textContent = msg;\n        t.style.display = 'block';\n        setTimeout(function() { t.style.display = 'none'; }, 3000);\n      }\n    }\n\n    function loadAllData() {\n      showToast('🔄 正在同步試算表最新資料...');\n      if (typeof google !== 'undefined' && google.script && google.script.run) {\n        google.script.run\n          .withSuccessHandler(function(data) {\n            if (data && typeof data === 'object') {\n              APP_DATA = data;\n              if (!APP_DATA.sheets) APP_DATA.sheets = {};\n              if (!APP_DATA.stats) APP_DATA.stats = { totalExpense: 0, missingCount: 0, todayContact: null, contactCount: 0 };\n            }\n            renderAll();\n          })\n          .withFailureHandler(function(err) {\n            console.error('讀取試算表失敗:', err);\n            showToast('⚠️ 讀取試算表失敗,已顯示離線介面');\n            renderAll();\n          })\n          .getAllSheetRecords();\n      } else {\n        renderAll();\n      }\n    }\n\n    function renderAll() {\n      var stats = (APP_DATA && APP_DATA.stats) ? APP_DATA.stats : { totalExpense: 0, missingCount: 0, todayContact: null, contactCount: 0 };\n      \n      var elContacts = document.getElementById('statContacts');\n      if (elContacts) elContacts.textContent = (stats.contactCount || 0) + ' 位聯絡人';\n\n      var elExpense = document.getElementById('statExpense');\n      if (elExpense) elExpense.textContent = 'NT$ ' + (stats.totalExpense || 0).toLocaleString() + ' 元';\n\n      var elMissing = document.getElementById('statMissing');\n      if (elMissing) elMissing.textContent = (stats.missingCount || 0) + ' 筆待補交';\n\n      var elContact = document.getElementById('statContact');\n      if (elContact) {\n        if (stats.todayContact && stats.todayContact.length > 0) {\n          elContact.textContent = (stats.todayContact[0] || '').substring(0, 10) + ' 已發布';\n        } else {\n          elContact.textContent = '尚無紀錄';\n        }\n      }\n      \n      var tabsContainer = document.getElementById('tabsList');\n      if (tabsContainer) {\n        tabsContainer.innerHTML = '';\n        TAB_CONFIG.forEach(function(tab) {\n          var btn = document.createElement('button');\n          btn.className = 'tab-btn' + (tab.name === currentSheetName ? ' active' : '');\n          btn.innerHTML = '<span>' + tab.icon + '</span> ' + tab.name;\n          btn.onclick = function() { selectTab(tab.name, btn); };\n          tabsContainer.appendChild(btn);\n        });\n      }\n      renderSheet(currentSheetName);\n    }\n\n    function selectTab(sheetName, btnElement) {\n      currentSheetName = sheetName;\n      document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('active'); });\n      if (btnElement) btnElement.classList.add('active');\n      renderSheet(sheetName);\n    }\n\n    function renderSheet(sheetName) {\n      var titleEl = document.getElementById('currentTabTitle');\n      if (titleEl) titleEl.textContent = sheetName;\n\n      var sheets = (APP_DATA && APP_DATA.sheets) ? APP_DATA.sheets : {};\n      var sheetData = sheets[sheetName];\n      var tableContent = document.getElementById('tableContent');\n      var countEl = document.getElementById('currentTabCount');\n\n      if (!sheetData || !sheetData.rows || sheetData.rows.length === 0) {\n        if (countEl) countEl.textContent = '尚無紀錄';\n        if (tableContent) {\n          tableContent.innerHTML = '<div class=\"empty-state\">📑 目前「' + sheetName + '」分頁尚無任何紀錄,可點擊上方「➕ 新增紀錄」或在 LINE 發送訊息!</div>';\n        }\n        return;\n      }\n\n      if (countEl) countEl.textContent = '共 ' + sheetData.rows.length + ' 筆紀錄';\n      var html = '<table class=\"data-table\"><thead><tr>';\n      (sheetData.headers || []).forEach(function(h) { html += '<th>' + h + '</th>'; });\n      html += '<th style=\"text-align:right;\">⚙️ 操作</th></tr></thead><tbody>';\n\n      sheetData.rows.forEach(function(item) {\n        var actualRow = item.actualRow;\n        var row = item.values || [];\n        html += '<tr>';\n        row.forEach(function(cell, colIdx) {\n          var text = (cell !== undefined && cell !== null) ? cell.toString() : '';\n          var headerName = (sheetData.headers && sheetData.headers[colIdx]) || '';\n          var formatted = text.replace(/\\n/g, '<br>');\n          \n          if (headerName.includes('狀態') || headerName.includes('狀況') || headerName.includes('進度') || headerName.includes('追蹤')) {\n            if (text === '待辦') {\n              formatted = '<span class=\"badge-tag badge-orange btn-toggle-status\" title=\"點擊切換為已完成\" data-sheet=\"' + sheetName + '\" data-row=\"' + actualRow + '\" data-target=\"已完成\">⏳ 待辦 (點擊完成)</span>';\n            } else if (text === '已完成') {\n              formatted = '<span class=\"badge-tag badge-green btn-toggle-status\" title=\"點擊切換為待辦\" data-sheet=\"' + sheetName + '\" data-row=\"' + actualRow + '\" data-target=\"待辦\">✅ 已完成 (點擊重開)</span>';\n            } else if (text.includes('未補交') || text.includes('待追蹤')) {\n              formatted = '<span class=\"badge-tag badge-red btn-toggle-status\" title=\"點擊切換為已補交\" data-sheet=\"' + sheetName + '\" data-row=\"' + actualRow + '\" data-target=\"已補交\">⚠️ 未補交 (點擊補交)</span>';\n            } else if (text === '已補交') {\n              formatted = '<span class=\"badge-tag badge-green btn-toggle-status\" title=\"點擊切換為未補交\" data-sheet=\"' + sheetName + '\" data-row=\"' + actualRow + '\" data-target=\"未補交\">✅ 已補交 (點擊重開)</span>';\n            } else if (text.includes('已同步')) {\n              formatted = '<span class=\"badge-tag badge-blue\">🗓️ ' + text + '</span>';\n            }\n          } else if (text === '支出') {\n            formatted = '<span class=\"badge-tag badge-red\">支出</span>';\n          } else if (text === '收入') {\n            formatted = '<span class=\"badge-tag badge-green\">收入</span>';\n          } else if (text.startsWith('http')) {\n            formatted = '<a href=\"' + text + '\" target=\"_blank\" style=\"color:var(--accent-blue); text-decoration:none; font-weight:600;\">🔗 開啟連結</a>';\n          }\n          html += '<td>' + formatted + '</td>';\n        });\n        html += '<td style=\"text-align:right; white-space:nowrap;\">' +\n          '<button class=\"action-btn btn-edit-row\" data-sheet=\"' + sheetName + '\" data-row=\"' + actualRow + '\">✏️ 編輯</button>' +\n          '<button class=\"action-btn btn-del btn-delete-row\" data-sheet=\"' + sheetName + '\" data-row=\"' + actualRow + '\">🗑️ 刪除</button>' +\n        '</td>';\n        html += '</tr>';\n      });\n      html += '</tbody></table>';\n      if (tableContent) tableContent.innerHTML = html;\n    }\n\n    // 事件委派:統一處理表格內所有按鈕點擊,零跳脫錯誤\n    var tc = document.getElementById('tableContent');\n    if (tc) {\n      tc.addEventListener('click', function(e) {\n        var toggleBtn = e.target.closest('.btn-toggle-status');\n        if (toggleBtn) {\n          var sheet = toggleBtn.getAttribute('data-sheet');\n          var row = toggleBtn.getAttribute('data-row');\n          var target = toggleBtn.getAttribute('data-target');\n          toggleStatus(sheet, row, target);\n          return;\n        }\n        var editBtn = e.target.closest('.btn-edit-row');\n        if (editBtn) {\n          var sheet = editBtn.getAttribute('data-sheet');\n          var row = editBtn.getAttribute('data-row');\n          openEditModal(sheet, row);\n          return;\n        }\n        var delBtn = e.target.closest('.btn-delete-row');\n        if (delBtn) {\n          var sheet = delBtn.getAttribute('data-sheet');\n          var row = delBtn.getAttribute('data-row');\n          deleteRow(sheet, row);\n          return;\n        }\n      });\n    }\n\n    function toggleStatus(sheetName, actualRow, newStatus) {\n      showToast('⏳ 正在更新狀態為「' + newStatus + '」...');\n      if (typeof google !== 'undefined' && google.script && google.script.run) {\n        google.script.run\n          .withSuccessHandler(function(res) {\n            if (res && res.success) {\n              showToast('✅ ' + res.message);\n              if (APP_DATA.sheets && APP_DATA.sheets[sheetName]) {\n                var sheetData = APP_DATA.sheets[sheetName];\n                var target = sheetData.rows.find(function(r) { return r.actualRow == actualRow; });\n                if (target) {\n                  var headers = sheetData.headers;\n                  var sCol = headers.findIndex(function(h) { return h.includes('狀態') || h.includes('狀況') || h.includes('進度') || h.includes('追蹤'); });\n                  if (sCol !== -1) target.values[sCol] = newStatus;\n                  renderSheet(sheetName);\n                }\n              }\n            } else { alert('更新失敗: ' + (res ? res.error : '未知錯誤')); }\n          })\n          .updateSheetStatus(sheetName, actualRow, newStatus);\n      }\n    }\n\n    function deleteRow(sheetName, actualRow) {\n      if (!confirm('確定要刪除這筆紀錄嗎?此動作將同步從 Google 試算表中移除。')) return;\n      showToast('⏳ 正在刪除紀錄...');\n      if (typeof google !== 'undefined' && google.script && google.script.run) {\n        google.script.run\n          .withSuccessHandler(function(res) {\n            if (res && res.success) {\n              showToast('🗑️ ' + res.message);\n              if (APP_DATA.sheets && APP_DATA.sheets[sheetName]) {\n                var sheetData = APP_DATA.sheets[sheetName];\n                sheetData.rows = sheetData.rows.filter(function(r) { return r.actualRow != actualRow; });\n                renderSheet(sheetName);\n              }\n            } else { alert('刪除失敗: ' + (res ? res.error : '未知錯誤')); }\n          })\n          .deleteSheetRow(sheetName, actualRow);\n      }\n    }\n\n    function openAddModal() {\n      editingActualRow = null;\n      document.getElementById('modalTitle').textContent = '➕ 新增「' + currentSheetName + '」紀錄';\n      var sheets = (APP_DATA && APP_DATA.sheets) ? APP_DATA.sheets : {};\n      var sheetData = sheets[currentSheetName] || { headers: ['紀錄日期', '內容', '備註'] };\n      var formFields = document.getElementById('formFields');\n      formFields.innerHTML = '';\n      (sheetData.headers || []).forEach(function(h, idx) {\n        var defaultVal = (idx === 0) ? new Date().toISOString().substring(0, 10).replace(/-/g, '/') : '';\n        formFields.innerHTML += '<div class=\"form-group\"><label>' + h + '</label><input type=\"text\" class=\"form-control\" name=\"field_' + idx + '\" value=\"' + defaultVal + '\" required></div>';\n      });\n      document.getElementById('editModal').style.display = 'flex';\n    }\n\n    function openEditModal(sheetName, actualRow) {\n      editingActualRow = actualRow;\n      document.getElementById('modalTitle').textContent = '✏️ 編輯「' + sheetName + '」紀錄';\n      var sheets = (APP_DATA && APP_DATA.sheets) ? APP_DATA.sheets : {};\n      var sheetData = sheets[sheetName];\n      if (!sheetData) return;\n      var target = sheetData.rows.find(function(r) { return r.actualRow == actualRow; });\n      if (!target) return;\n      var formFields = document.getElementById('formFields');\n      formFields.innerHTML = '';\n      (sheetData.headers || []).forEach(function(h, idx) {\n        var val = target.values[idx] || '';\n        formFields.innerHTML += '<div class=\"form-group\"><label>' + h + '</label><input type=\"text\" class=\"form-control\" name=\"field_' + idx + '\" value=\"' + String(val).replace(/\"/g, '&quot;') + '\" required></div>';\n      });\n      document.getElementById('editModal').style.display = 'flex';\n    }\n\n    function closeModal() { document.getElementById('editModal').style.display = 'none'; }\n\n    function handleFormSubmit(e) {\n      e.preventDefault();\n      var form = e.target;\n      var sheets = (APP_DATA && APP_DATA.sheets) ? APP_DATA.sheets : {};\n      var sheetData = sheets[currentSheetName] || { headers: [] };\n      var rowData = [];\n      (sheetData.headers || []).forEach(function(h, idx) {\n        rowData.push(form.elements['field_' + idx].value);\n      });\n      var isEdit = (editingActualRow !== null);\n      showToast(isEdit ? '⏳ 正在儲存修改...' : '⏳ 正在新增紀錄...');\n\n      if (typeof google !== 'undefined' && google.script && google.script.run) {\n        if (isEdit) {\n          google.script.run.withSuccessHandler(function(res) {\n            if (res && res.success) {\n              showToast('🎉 ' + res.message);\n              closeModal();\n              loadAllData();\n            } else { alert('儲存失敗: ' + (res ? res.error : '未知錯誤')); }\n          }).editSheetRow(currentSheetName, editingActualRow, rowData);\n        } else {\n          google.script.run.withSuccessHandler(function(res) {\n            if (res && res.success) {\n              showToast('🎉 ' + res.message);\n              closeModal();\n              loadAllData();\n            } else { alert('儲存失敗: ' + (res ? res.error : '未知錯誤')); }\n          }).addSheetRow(currentSheetName, rowData);\n        }\n      }\n    }\n\n    loadAllData();\n  </script>\n</body>\n</html>";
}

// 儀表板:更新狀態
function updateSheetStatus(sheetName, actualRow, newStatus) {
  try {
    const ss = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID);
    const sheet = ss.getSheetByName(sheetName);
    if (!sheet) return { success: false, error: '找不到分頁: ' + sheetName };
    const row = parseInt(actualRow, 10);
    if (row >= 2 && row <= sheet.getLastRow()) {
      const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
      let statusCol = -1;
      headers.forEach((h, idx) => {
        if (h.includes('狀態') || h.includes('狀況') || h.includes('進度') || h.includes('追蹤')) {
          statusCol = idx + 1;
        }
      });
      if (statusCol !== -1) {
        sheet.getRange(row, statusCol).setValue(newStatus);
        return { success: true, message: '狀態已更新為「' + newStatus + '」' };
      }
    }
    return { success: false, error: '未找到狀態欄位或列數無效' };
  } catch (err) {
    return { success: false, error: err.toString() };
  }
}

// 儀表板:刪除指定列
function deleteSheetRow(sheetName, actualRow) {
  try {
    const ss = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID);
    const sheet = ss.getSheetByName(sheetName);
    if (!sheet) return { success: false, error: '找不到分頁: ' + sheetName };
    const row = parseInt(actualRow, 10);
    if (row >= 2 && row <= sheet.getLastRow()) {
      sheet.deleteRow(row);
      return { success: true, message: '已成功刪除該筆紀錄' };
    }
    return { success: false, error: '列數無效' };
  } catch (err) {
    return { success: false, error: err.toString() };
  }
}

// 儀表板:新增一筆紀錄
function addSheetRow(sheetName, rowData) {
  try {
    const ss = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID);
    const sheet = ss.getSheetByName(sheetName);
    if (!sheet) return { success: false, error: '找不到分頁: ' + sheetName };
    sheet.appendRow(rowData);
    return { success: true, message: '已成功新增紀錄' };
  } catch (err) {
    return { success: false, error: err.toString() };
  }
}

// 儀表板:編輯更新整列
function editSheetRow(sheetName, actualRow, rowData) {
  try {
    const ss = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID);
    const sheet = ss.getSheetByName(sheetName);
    if (!sheet) return { success: false, error: '找不到分頁: ' + sheetName };
    const row = parseInt(actualRow, 10);
    if (row >= 2 && row <= sheet.getLastRow()) {
      sheet.getRange(row, 1, 1, rowData.length).setValues([rowData]);
      return { success: true, message: '已成功更新紀錄' };
    }
    return { success: false, error: '列數無效' };
  } catch (err) {
    return { success: false, error: err.toString() };
  }
}

// ==================== 11. 一鍵初始化與日曆/聯絡人授權測試函式 ====================
function initAllSheets() {
  const ssId = CONFIG.SPREADSHEET_ID;
  if (!ssId || ssId === 'YOUR_SPREADSHEET_ID') {
    Logger.log('❌ 請先填寫 SPREADSHEET_ID!');
    return;
  }
  const ss = SpreadsheetApp.openById(ssId);
  ensureSheetHeaders(ss);
  Logger.log('✅ 11 大專屬分頁與美化表頭初始化完成!');
}

function testAllPermissions() {
  Logger.log('🚀 開始檢測並打通【Google 日曆 & Google 聯絡人】雙向授權...');
  
  // 1. 測試 Google 日曆連線
  try {
    const calId = testCreateCalendarEvent();
    Logger.log('✅ Google 日曆授權與活動建立測試成功!');
  } catch (e) {
    Logger.log('❌ Google 日曆測試異常: ' + e.message);
  }

  // 2. 測試 Google 聯絡人連線
  try {
    testCreateContact();
    Logger.log('✅ Google 聯絡人授權與通訊錄建立測試成功!');
  } catch (e) {
    Logger.log('❌ Google 聯絡人測試異常: ' + e.message);
  }

  Logger.log('🎉 【恭喜】Google 日曆 & Google 聯絡人雙向雲端授權已 100% 打通完成!');
}

function testCreateCalendarEvent() {
  const cal = CalendarApp.getDefaultCalendar();
  const sampleTime = new Date(Date.now() + 24 * 60 * 60 * 1000);
  const event = cal.createEvent('【測試】全能管家日曆連線成功', sampleTime, new Date(sampleTime.getTime() + 3600000), {
    description: '恭喜!您的 LINE 智慧管家已成功取得 Google 日曆授權!'
  });
  Logger.log('🎉 Google 日曆授權並建立成功,活動 ID: ' + event.getId());
  return event.getId();
}

function testCreateContact() {
  const sample = createGoogleContact({
    name: '測試聯絡人',
    phone: '0912-345678',
    email: 'test@example.com',
    company_title: '測試學校 教師'
  });
  Logger.log('🎉 Google 聯絡人授權並建立成功: ' + JSON.stringify(sample));
}


// ==================== 5. 旗艦 3 分頁圖文選單全自動安裝與管理 ====================

/**
 * 建立並發布 3 分頁切換式旗艦圖文選單 (Tab 1: 日常反查, Tab 2: 事後改存, Tab 3: 事前鎖定)
 */
function setupMultiTabRichMenus() {
  const token = CONFIG.LINE_ACCESS_TOKEN;
  if (!token || token === 'YOUR_LINE_ACCESS_TOKEN') {
    throw new Error('未設定 LINE_ACCESS_TOKEN,請先在指令碼屬性中填入金鑰!');
  }

  // 取得使用者個人的 Web App 儀表板網址(優先讀取手動設定的 WEB_APP_URL)
  let webAppUrl = PropertiesService.getScriptProperties().getProperty('WEB_APP_URL');
  if (!webAppUrl || !webAppUrl.startsWith('http')) {
    try {
      webAppUrl = ScriptApp.getService().getUrl();
    } catch(e) {}
  }
  // 濾除非公開發布網址或無效連結
  if (!webAppUrl || !webAppUrl.startsWith('http') || webAppUrl.includes('/edit') || webAppUrl.includes('/dev')) {
    webAppUrl = 'https://ijun-ai.com/tutor/line-bot/';
  }

  const cdnBase = 'https://ijun-ai.com/tutor/line-bot/assets/';
  const images = {
    tab1: cdnBase + 'richmenu_tab1_query.jpg',
    tab2: cdnBase + 'richmenu_tab2_remap.jpg',
    tab3: cdnBase + 'richmenu_tab3_lock.jpg'
  };

  // 先清理舊的 Alias 與選單以防衝突
  try { deleteMultiTabRichMenus(); } catch(e) { Logger.log('清理舊選單完成或無舊選單: ' + e.message); }

  // 座標尺寸
  const colW = 833;
  const colW3 = 834;
  const rowH = 360;
  const tabH = 240;

  // 定義 3 大分頁的頂部切換區塊 (使用 richmenuswitch 瞬時切換)
  const topTabs = (currentTab) => [
    {
      bounds: { x: 0, y: 0, width: colW, height: tabH },
      action: (currentTab === 1) ? { type: 'message', text: '/查 今日' } : { type: 'richmenuswitch', richMenuAliasId: 'tab_query', data: 'tab1' }
    },
    {
      bounds: { x: colW, y: 0, width: colW, height: tabH },
      action: (currentTab === 2) ? { type: 'message', text: '/undo' } : { type: 'richmenuswitch', richMenuAliasId: 'tab_remap', data: 'tab2' }
    },
    {
      bounds: { x: colW * 2, y: 0, width: colW3, height: tabH },
      action: (currentTab === 3) ? { type: 'message', text: '/cancel' } : { type: 'richmenuswitch', richMenuAliasId: 'tab_lock', data: 'tab3' }
    }
  ];

  // 12 格座標產生器 (4 列 x 3 欄)
  function getGridAreas(currentTab, gridActions) {
    const areas = topTabs(currentTab);
    for (let r = 0; r < 4; r++) {
      for (let c = 0; c < 3; c++) {
        const idx = r * 3 + c;
        const x = (c === 2) ? (colW * 2) : (c * colW);
        const w = (c === 2) ? colW3 : colW;
        const y = tabH + r * rowH;
        const act = gridActions[idx];
        if (act) {
          areas.push({
            bounds: { x: x, y: y, width: w, height: rowH },
            action: act
          });
        }
      }
    }
    return areas;
  }

  // --- Tab 1 Actions (日常反查與功能) ---
  const tab1Actions = [
    { type: 'message', text: '/查 今日' },
    { type: 'message', text: '/查 進度' },
    { type: 'message', text: '/查 缺交' },

    { type: 'message', text: '/查 行事曆' },
    { type: 'message', text: '/查 家長' },
    { type: 'message', text: '/查 學生' },

    { type: 'message', text: '/查 備課' },
    { type: 'message', text: '/查 雜事' },
    { type: 'message', text: '/查 花費' },

    { type: 'message', text: '/查 聯絡人' },
    { type: 'uri', uri: webAppUrl },
    { type: 'uri', uri: 'https://ijun-ai.com/tutor/line-bot/commands.html' }
  ];

  // --- Tab 2 Actions (事後改存) ---
  const tab2Actions = [
    { type: 'message', text: '改存 聯絡人' },
    { type: 'message', text: '改存 行事曆' },
    { type: 'message', text: '改存 記帳' },

    { type: 'message', text: '改存 教學進度' },
    { type: 'message', text: '改存 今日聯絡簿' },
    { type: 'message', text: '改存 備課靈感' },

    { type: 'message', text: '改存 作業缺交' },
    { type: 'message', text: '改存 學生概況' },
    { type: 'message', text: '改存 家長聯繫' },

    { type: 'message', text: '改存 生活雜事' },
    { type: 'message', text: '改存 好物分享' },
    { type: 'message', text: '/undo' }
  ];

  // --- Tab 3 Actions (事前鎖定) ---
  const tab3Actions = [
    { type: 'message', text: '/名片' },
    { type: 'message', text: '/行事曆' },
    { type: 'message', text: '/記帳' },

    { type: 'message', text: '/進度' },
    { type: 'message', text: '/聯絡簿' },
    { type: 'message', text: '/靈感' },

    { type: 'message', text: '/缺交' },
    { type: 'message', text: '/學生' },
    { type: 'message', text: '/家長' },

    { type: 'message', text: '/雜事' },
    { type: 'message', text: '/好物' },
    { type: 'message', text: '/cancel' }
  ];

  // 1. 先建立一個無 richmenuswitch 的暫存 Dummy Menu,用以初始化 3 個 Alias
  const dummyId = createSingleRichMenu('init_temp', [{
    bounds: { x: 0, y: 0, width: 100, height: 100 },
    action: { type: 'message', text: 'init' }
  }], token);

  // 2. 初始化 3 個 Alias 指向 dummyId(解決 LINE API 先有 Alias 才能建 switch 的依賴限制)
  createOrUpdateRichMenuAlias('tab_query', dummyId, token);
  createOrUpdateRichMenuAlias('tab_remap', dummyId, token);
  createOrUpdateRichMenuAlias('tab_lock', dummyId, token);

  // 3. 正式建立 3 個包含 richmenuswitch 的真實 Rich Menu
  const menu1Id = createSingleRichMenu('日常反查與功能', getGridAreas(1, tab1Actions), token);
  const menu2Id = createSingleRichMenu('事後一鍵改存', getGridAreas(2, tab2Actions), token);
  const menu3Id = createSingleRichMenu('事前強制鎖定', getGridAreas(3, tab3Actions), token);

  // 4. 上傳 3 張高畫質底圖
  uploadRichMenuImage(menu1Id, images.tab1, token);
  uploadRichMenuImage(menu2Id, images.tab2, token);
  uploadRichMenuImage(menu3Id, images.tab3, token);

  // 5. 更新 3 個 Alias 正式指向真實選單
  createOrUpdateRichMenuAlias('tab_query', menu1Id, token);
  createOrUpdateRichMenuAlias('tab_remap', menu2Id, token);
  createOrUpdateRichMenuAlias('tab_lock', menu3Id, token);

  // 6. 設定 Tab 1 為所有使用者預設選單
  setDefaultRichMenu(menu1Id, token);

  // 7. 清理暫存 Dummy Menu
  try {
    UrlFetchApp.fetch('https://api.line.me/v2/bot/richmenu/' + dummyId, {
      method: 'delete',
      headers: { 'Authorization': 'Bearer ' + token },
      muteHttpExceptions: true
    });
  } catch(e) {}

  // 8. 儲存 ID 至 ScriptProperties
  PropertiesService.getScriptProperties().setProperties({
    'RICHMENU_ID_TAB1': menu1Id,
    'RICHMENU_ID_TAB2': menu2Id,
    'RICHMENU_ID_TAB3': menu3Id
  });

  return {
    success: true,
    message: '🎉 恭喜!全套「3 分頁切換式旗艦圖文選單」已成功全自動安裝並發布!\n━━━━━━━━━━━━━━\n• 🔍 分頁 1:日常反查與功能(已動態綁定個人專屬儀表板)\n• 🔄 分頁 2:事後一鍵改存\n• 🎯 分頁 3:事前強制鎖定\n\n💡 請重新關閉再打開手機 LINE 聊天室,立即體驗毫秒級切換!'
  };
}

/**
 * 建立或更新 Rich Menu Alias
 */
function createOrUpdateRichMenuAlias(aliasId, richMenuId, token) {
  // 先嘗試建立
  const createUrl = 'https://api.line.me/v2/bot/richmenu/alias';
  const payload = {
    richMenuAliasId: aliasId,
    richMenuId: richMenuId
  };

  const createOptions = {
    method: 'post',
    contentType: 'application/json',
    headers: { 'Authorization': 'Bearer ' + token },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };

  const res = UrlFetchApp.fetch(createUrl, createOptions);
  if (res.getResponseCode() === 400 || res.getResponseCode() === 409) {
    // 若已存在則更新
    const updateUrl = 'https://api.line.me/v2/bot/richmenu/alias/' + aliasId;
    const updateOptions = {
      method: 'post',
      contentType: 'application/json',
      headers: { 'Authorization': 'Bearer ' + token },
      payload: JSON.stringify({ richMenuId: richMenuId }),
      muteHttpExceptions: true
    };
    UrlFetchApp.fetch(updateUrl, updateOptions);
  }
}

function createSingleRichMenu(name, areas, token) {
  const url = 'https://api.line.me/v2/bot/richmenu';
  const payload = {
    size: { width: 2500, height: 1686 },
    selected: true,
    name: name,
    chatBarText: '📋 開啟智慧管家選單',
    areas: areas
  };

  const options = {
    method: 'post',
    contentType: 'application/json',
    headers: { 'Authorization': 'Bearer ' + token },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };

  const response = UrlFetchApp.fetch(url, options);
  const resCode = response.getResponseCode();
  const resText = response.getContentText();

  if (resCode !== 200) {
    throw new Error('建立選單「' + name + '」失敗 (' + resCode + '): ' + resText);
  }

  const json = JSON.parse(resText);
  return json.richMenuId;
}

/**
 * 上傳 Rich Menu 底圖
 */
function uploadRichMenuImage(richMenuId, imageUrl, token) {
  // 從 CDN 下載圖片 Blob
  const imgRes = UrlFetchApp.fetch(imageUrl, { muteHttpExceptions: true });
  if (imgRes.getResponseCode() !== 200) {
    throw new Error('下載選單底圖失敗 (' + imgRes.getResponseCode() + '): ' + imageUrl);
  }
  const contentType = (imageUrl.toLowerCase().endsWith('.jpg') || imageUrl.toLowerCase().endsWith('.jpeg')) ? 'image/jpeg' : 'image/png';
  const imgBlob = imgRes.getBlob().setContentType(contentType);

  const uploadUrl = 'https://api-data.line.me/v2/bot/richmenu/' + richMenuId + '/content';
  const options = {
    method: 'post',
    contentType: contentType,
    headers: { 'Authorization': 'Bearer ' + token },
    payload: imgBlob.getBytes(),
    muteHttpExceptions: true
  };

  const response = UrlFetchApp.fetch(uploadUrl, options);
  const resCode = response.getResponseCode();
  if (resCode !== 200) {
    throw new Error('上傳選單底圖至 LINE 失敗 (' + resCode + '): ' + response.getContentText());
  }
}

/**
 * 建立 Rich Menu Alias
 */
function createRichMenuAlias(aliasId, richMenuId, token) {
  const url = 'https://api.line.me/v2/bot/richmenu/alias';
  const payload = {
    richMenuAliasId: aliasId,
    richMenuId: richMenuId
  };

  const options = {
    method: 'post',
    contentType: 'application/json',
    headers: { 'Authorization': 'Bearer ' + token },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };

  UrlFetchApp.fetch(url, options);
}

/**
 * 設定全體使用者預設選單
 */
function setDefaultRichMenu(richMenuId, token) {
  const url = 'https://api.line.me/v2/bot/user/all/richmenu/' + richMenuId;
  const options = {
    method: 'post',
    headers: { 'Authorization': 'Bearer ' + token },
    muteHttpExceptions: true
  };

  const response = UrlFetchApp.fetch(url, options);
  if (response.getResponseCode() !== 200) {
    throw new Error('設定預設選單失敗: ' + response.getContentText());
  }
}

/**
 * 移除並清理所有 API 圖文選單
 */
function deleteMultiTabRichMenus() {
  const token = CONFIG.LINE_ACCESS_TOKEN;
  if (!token || token === 'YOUR_LINE_ACCESS_TOKEN') return { success: false, message: '未設定 Token' };

  // 1. 取消預設選單
  try {
    UrlFetchApp.fetch('https://api.line.me/v2/bot/user/all/richmenu', {
      method: 'delete',
      headers: { 'Authorization': 'Bearer ' + token },
      muteHttpExceptions: true
    });
  } catch(e) {}

  // 2. 刪除 Aliases
  ['tab_query', 'tab_remap', 'tab_lock'].forEach(alias => {
    try {
      UrlFetchApp.fetch('https://api.line.me/v2/bot/richmenu/alias/' + alias, {
        method: 'delete',
        headers: { 'Authorization': 'Bearer ' + token },
        muteHttpExceptions: true
      });
    } catch(e) {}
  });

  // 3. 刪除所有 Rich Menu 清單
  try {
    const listRes = UrlFetchApp.fetch('https://api.line.me/v2/bot/richmenu/list', {
      method: 'get',
      headers: { 'Authorization': 'Bearer ' + token },
      muteHttpExceptions: true
    });
    if (listRes.getResponseCode() === 200) {
      const list = JSON.parse(listRes.getContentText()).richmenus || [];
      list.forEach(m => {
        UrlFetchApp.fetch('https://api.line.me/v2/bot/richmenu/' + m.richMenuId, {
          method: 'delete',
          headers: { 'Authorization': 'Bearer ' + token },
          muteHttpExceptions: true
        });
      });
    }
  } catch(e) {}

  PropertiesService.getScriptProperties().deleteProperty('RICHMENU_ID_TAB1');
  PropertiesService.getScriptProperties().deleteProperty('RICHMENU_ID_TAB2');
  PropertiesService.getScriptProperties().deleteProperty('RICHMENU_ID_TAB3');

  return {
    success: true,
    message: '🗑️ 已成功移除所有 API 圖文選單!手機將自動恢復為官方後台預設設定。'
  };
}
2
步驟 2:確認專屬網址屬性(WEB_APP_URL)

點擊 Apps Script 左側齒輪 ⚙️ 「專案設定」 ➔ 拉到最下方「指令碼屬性」:

  • 若已有 WEB_APP_URL:確認內容是自己部署的 https://script.google.com/macros/s/.../exec 網址。
  • 若沒有:點擊「新增指令碼屬性」➔ 屬性輸入 WEB_APP_URL、值填入自己的 /exec 網址 ➔ 點擊「儲存指令碼屬性」。
3
步驟 3:一鍵安裝全新 3 分頁選單

點擊左側程式碼 < > 回到編輯器:

  1. 上方函式選單下拉選取 setupMultiTabRichMenus
  2. 點擊 ▶️ 「執行」 按鈕。
  3. 等待約 3~5 秒看到日誌顯示 🎉 恭喜!全套「3 分頁切換式旗艦圖文選單」已成功全自動安裝並發布!
4
步驟 4:更新雲端部署版本(讓新指令正式生效)

在 Apps Script 畫面右上角:

  1. 點擊右上角 「部署」➔「管理部署作業」
  2. 點擊右上角鉛筆圖示 「✏️ 編輯」
  3. 「版本」下拉選單選取 「新版本」
  4. 點擊右下角 「部署」 ➔ 點擊「完成」。

🎉 大功告成!將手機 LINE 聊天室關閉再重新點開,即可享受全新 3 分頁立體晶透旗艦選單!

🛠️

4. 【手動設定方式】LINE 官方帳號管理後台 6 格傳統選單設定

如果您想自行在 LINE 官方後台手動拉框、設定顯示時間或套用傳統 6 格版型,請參考此處步驟。

1
更換機器人頭像、封面背景與帳號名稱(打造專屬門面)

在設定圖文選單前,建議先為管家換上專屬的頭像與封面背景,讓聊天室更具專業與親和力:

  1. 前往 LINE Official Account Manager 後台 登入您的官方帳號,選擇您的「教師智慧管家」。
  2. 點選右上角 「設定」➔「基本檔案」 或點選頁面中的 「編輯」
  3. 更換頭像圖片:點選「基本檔案圖片」並上傳圖片。
  4. 上傳封面背景圖片:點選「背景圖片」上傳封面橫幅圖片。
  5. 公開發布:確認圖片與名稱無誤後,點擊右上角的綠色 【公開】 按鈕,即完成專屬門面設定!
頭像參考圖
🖼️ 頭像參考圖
奕鈞老師手作風格頭像
⬇️ 下載頭像 JPG
封面背景參考圖
🖼️ 封面背景參考圖
精美教師管家橫幅封面
⬇️ 下載封面 JPG
2
登入 LINE 官方帳號管理後台並建立圖文選單

點選後台左側選單 「聊天室相關」「圖文選單」,接著點擊右上角的綠色按鈕 「建立」

3
設定顯示期間與選單標題

在基本設定區塊中依序完成以下項目:

  • 標題:填寫方便後台辨識的名稱(例如:教師管家 6 格選單)。
  • 使用期間:請將「開始時間」設為今天,「結束時間」直接設定為 數年後(例如:2030 年),確保選單常駐不中斷。
4
選擇 6 格版型並上傳底圖

在內容設定區塊中:

  1. 點擊 「選擇版型」 ➔ 挑選 大型 6 格版型(3 欄 × 2 列)。
  2. 點擊 「上傳背景圖片」 ➔ 選擇下方提供的任一款 6 格風格 JPG 底圖。
5
設定 A~F 區塊動作與儲存發布

依序為 A~F 格設定對應的文字動作:

  • A 區塊:類型選「文字」➔ 輸入 /查 今日
  • B 區塊:類型選「文字」➔ 輸入 /查 進度
  • C 區塊:類型選「文字」➔ 輸入 /查 缺交
  • D 區塊:類型選「文字」➔ 輸入 /查 聯絡人
  • E 區塊:類型選「文字」➔ 輸入 /查 花費
  • F 區塊:類型選「文字」➔ 輸入 /help

最後將「預設顯示方式」設為 「顯示」,點擊最下方 「儲存」 即可發布生效!

🎨

5. 6 款專屬風格圖文選單免費下載 (JPG)

完全符合 LINE 官方標準 2500 × 1686 規格(檔案均小於 1MB),點擊直接下載!

6. 常見問題與操作秘笈 Q&A (FAQ)

快速解答關於多模態拍照、語音直錄與快捷管理指令的所有疑問!

Q1 📸 【拍照與多模態上傳】支援哪些功能?
管家整合 Gemini AI 多模態視覺辨識模型,直接在 LINE 聊天室傳送照片即可自動歸檔:
  • 拍名片 ➜ 自動解析姓名電話,存入試算表並同步 Google 聯絡人通訊錄。
  • 拍發票 / 收據 ➜ 自動解析總金額、品項明細與店家名稱,存入「記帳本」分頁。
  • 拍研習海報 / 公文 ➜ 自動擷取研習時間與地點,並即時在您的 Google 日曆 同步新增活動。
  • 拍黑白板 / 手寫筆記 ➜ 拍照課堂手寫字自動整理為重點綱要,歸檔至「備課靈感」教案庫。
Q2 🎙️ 【語音直錄】如何隨時說話記筆記?
在 LINE 聊天室長按麥克風直接說話即可:
  • 自動聽寫與智慧分流:AI 會自動將語音轉為文字,並根據內容自動判斷該歸檔到聯絡簿、家長聯繫、作業缺交或備忘錄等 11 大分頁。
  • • 適合開車途中、通話結束後或下課空檔隨口口述備忘。
Q3 ⚡ 【快捷管理指令】有哪些?如何一秒反查紀錄?
在對話框輸入下列指令(或點擊底部圖文選單按鈕),管家會即時為您反查與管理:
  • /安裝選單 ➜ 一鍵自動安裝與發布 3 分頁旗艦圖文選單
  • /網址 <URL> ➜ 綁定自己的專屬 Google Apps Script 儀表板網址
  • /查 進度 ➜ 查看各班最新教學章節進度與交代事項
  • /查 缺交 ➜ 查看全班各科未補交作業清單與座號
  • /查 今日 ➜ 查看今日聯絡簿公告與今日日曆行程
  • /查 花費 ➜ 查看記帳本本月累計總支出與近 3 筆消費明細
  • /查 聯絡人 ➜ 查看通訊錄電話與 Email
  • /del <關鍵字> ➜ 精準搜尋並自動刪除指定的紀錄項目
  • /undo ➜ 撤回並自動從 Google 試算表刪除上一筆存入的資料
  • /help ➜ 顯示完整指令手冊與 11 大分類輸入範例
Q4 🤖 【AI 諮詢提問】該怎麼下提示詞(Prompt)?
向管家請教教學與班級問題時,掌握黃金公式:【對象/年級】 + 【具體情境/痛點】 + 【希望產出的格式/限制】
  • 班級常規:「我是國一導師,兩位學生上課容易說話起鬨,請提供 3 個不打斷課堂節奏的引導與座次策略。」
  • 親師溝通:「學生在校擦傷已擦藥,請擬一段 150 字以內、語氣委婉但同理專業的 LINE 訊息傳給家長。」
  • 素養教案:「請為國二理化『浮力』設計 3 個由淺入深的引導提問,以及 1 個 10 分鐘小組探究活動。」
Q5 ☕ 也可以問生活上或其他非教育的問題嗎?
完全可以!它就是您 LINE 裡的隨身全能 Gemini AI 助手!
生活疑難、旅遊規劃與科技排錯隨時都能問:
  • • 🚗 旅遊規劃:「週末帶家人去宜蘭兩天一夜,有長輩小孩,推薦步調輕鬆的行程與餐廳。」
  • • 🍳 清冰箱料理:「冰箱剩雞蛋、洋蔥、番茄,請提供 1 道 15 分鐘簡單美味食譜。」
  • • 🌐 翻譯與排錯:「請把這段感謝信翻譯成道地日文」或「Excel VLOOKUP 出現 #N/A 怎麼解?」
Q6 ✨ 安裝 3 分頁選單會影響原本已有的設定或資料嗎?
完全不會! 程式建立圖文選單只會設定手機介面的選單外觀,原本試算表內的 11 大分頁紀錄、Google 聯絡人與日曆同步 100% 完整保留不受任何影響。
✅ 已複製到剪貼簿!