已成功複製到剪貼簿!
零伺服器費用 ✕ 支援平板車即時借用查詢 ✕ 附 8 大範例庫

零程式基礎!打造各校專屬的 LINE Bot 資訊組長 AI 智慧小幫手

告別每天被問 800 次「這節課誰借平板車?」、「Wi-Fi 怎麼連?」的靈魂拷問!整合 平板即時借用系統Google 試算表,24 小時為全校即時解答。

即時借用查詢

連線平板借用系統秒回借用人

Google 試算表

表格就是知識庫,隨改隨生效

Gemini 2.5 Flash

極速 RAG 推理,零幻覺精準回答

LINE Bot 串接

私訊或群組 @小幫手 都能用

最新實戰功能升級

⚡ 連線「校園平板與教室即時借用系統」

小幫手已整合線上借用系統(tablet-checkout.pages.dev/bookings),隨時用語音或文字問這節課誰借!

開啟借用系統前台
問:這節課是誰借平板?

🤖 AI 小幫手回答:

老師您好!目前時間是【第二節】(09:15-10:10),借用狀況如下:

📱 第九車:范慧蘭 老師

📱 第十車:楊千惠 老師

其餘第十一、十二、十四、十五車目前皆為空閒可借用喔!

問:第三節有沒有人借第九車?

🤖 AI 小幫手回答:

查核結果:第三節 (10:10-11:05) 的「第九車」目前無人登記借用,是空閒狀態!您可以直接至線上系統登記借用喔。

問:1樓卓越基地今天有誰借?

🤖 AI 小幫手回答:

今天 1樓卓越基地的登記紀錄:

🏫 借用人:文書組長

借用節次:第二節、第三節

📝 會議事由:115學年度第1次全校會議

核心知識庫維護

如何新增、修改後台知識庫資料?

打造好 AI 小幫手後,日後學校有新規定、新設備或新的常見問答時,您可以透過以下 3 種方式 隨時更新,完全不需要修改任何程式碼,也不用重新部署!

最直覺・最推薦

方法一:直接在 Google 試算表新增

打開試算表的「資訊組知識庫」工作表,在最底下一列填寫:

A欄 (分類):例如 平板與設備借用
B欄 (標題):例如 新進教師平板發放SOP
C欄 (內容):寫下步驟 1. 2. 3.
D欄 (狀態):填「啟用」
⚡ 特點:填好關閉即可,小幫手 1 秒內自動學會!
手機隨手記

方法二:手機 LINE 對話一鍵記憶

組長在外巡堂或修設備時,直接在 LINE 對小幫手傳送指令:

發送範例:
#記住 電腦教室2的投影機已更換新燈泡,目前可正常使用

Gemini 會自動結構化提取分類與標題,並自動追加一列寫入 Google 試算表!

📱 特點:不用開電腦,手機打字就完成知識庫更新!
視覺化檢視

方法三:透過 Web 控制台管理

點開部署產生的 Web App 網址(index.html 面板),您可以看到美麗的卡片清單:

  • 依「8 大業務分類」快速切換標籤。
  • 即時搜尋框,輸入關鍵字秒查內容。
  • 查看各項目的狀態與 ID 編號。
🖥️ 特點:適合向處室主管或全校同仁展示知識庫架構!
新任組長實戰必備

新任資訊組長常見 8 大核心業務範例庫

整理好台灣中小學最常見的 8 大資訊業務分類,已全面整合平板車線上借用系統!

1

校園網路與帳號

  • • 新進教師公務 Google/OpenID 開通
  • • 校園 802.1X Wi-Fi 連線設定
  • • eduroam 跨校漫遊帳號指引
2

數位教學與軟體

  • • Office 365 / Windows 授權認證
  • • Canva 教育版免費教師申請
  • • Google Classroom 班級開通
3

數位研習增能管考

  • • 教育部 A1、A2、A3 研習規範
  • • edu 磨課師+ 線上修課 SOP
  • • 全國在職進修網時數查詢
4

校務系統與行政

  • • 1Campus / 智慧校園導師開通
  • • 公文線上簽核系統登入
  • • 差勤請假系統權限申請
5

平板與空間借用 API連線

  • • 平板借用系統即時查詢
  • • 生生用平板車/充電車借還規程
  • • 電腦教室與卓越基地預約規範
6

校園資安與個資

  • • 資安紅線(嚴禁私設 AP 熱點)
  • • 社交工程釣魚郵件演練防範
  • • 公私信箱分流與密碼更換原則
7

教室設備簡易排障

  • • 投影機/大屏無畫面聲音排查 3 步
  • • 教室實體網路孔無法上網排查
  • • 筆電投影快捷鍵 Win+P 指引
8

設備報修與耗材

  • • 校園線上設備報修系統流程
  • • 緊急故障即時電話通報專線
  • • 公務耗材領用與登記流程

小學生都能跟著做!安裝 6 大步驟

跟著以下順序做,每完成一步就打勾,10 分鐘內就能上線!

進度:0%
1

建立 LINE 機器人,取得 Access Token

約 2 分鐘完成

  1. 進入 LINE Developers 後台 ,點右上角 Log in 使用您平時的 LINE 帳號登入。
  2. 點擊 Create a new provider,輸入您的組織名稱(例如:[學校名稱] 資訊組)。
  3. 在 Provider 內點選 Create a Messaging API channel
    • Channel name:輸入機器人名稱(例如:校園資訊組 AI 小幫手)
    • Channel description:輸入簡介(例如:協助全校同仁解決各項校園資訊、網路、研習與平板借用查詢)
    • Category:選擇「學校、教育」相關分類,並勾選同意條款後按下 Create
  4. 建立後點進該 Channel,切換到上方 Messaging API 頁籤:
    • 拉到頁面最下方找到 Channel access token,點擊 Issue 生成金鑰,並將這串很長的英文數字複製備用!
    • 在同頁面找到 Auto-reply messages,點進去將「自動回應訊息」改為 關閉、「Webhook」改為 啟用
2

取得免費 Google Gemini API Key

約 1 分鐘完成

  1. 前往 Google AI Studio 官網 ,登入您的 Google 帳號。
  2. 點擊左上角或右上角的 Get API key(取得 API 金鑰)。
  3. 點選 Create API key,選擇預設的 Google Cloud 專案並點擊建立。
  4. 複製生成的以 AIzaSy... 開頭的金鑰代碼,稍後填入後端即可(Google 提供超大額度的免費呼叫量,完全免綁信用卡)。
3

建立 Google 試算表(知識庫大腦)

約 1 分鐘完成

  1. 打開 Google 試算表 (sheets.new) ,建立一份全新空白試算表。
  2. 將試算表標題命名為:校園資訊組智慧小幫手知識庫
  3. 下方預設的工作表「工作表1」可以不用手動改名或手動貼資料,等一下貼好程式碼後執行一次「初始化函式」,它會自動幫您建立「資訊組知識庫」與「對話紀錄」並填入 8 大核心領域完整範例!
4

貼上 Apps Script 程式碼(Code.gs & index.html)

約 2 分鐘完成

  1. 在剛才建立的 Google 試算表上方選單,點選 擴充功能 ➔ Apps Script
  2. 進入編輯器後,將右側原本預設的 myFunction 全部刪除清空。
  3. 點擊下方【一鍵複製 Code.gs】,貼入 程式碼.gs (Code.gs) 編輯區,並在最上方的 CONFIG 中貼入您的 GEMINI_API_KEYLINE_ACCESS_TOKEN
  4. 點擊左側檔案列表上方的 「+」號 ➔ 選擇「HTML」,將檔案名稱命名為 index(請直接輸入 index,系統會自動補上 .html)。
  5. 點擊下方【一鍵複製 index.html】,將內容貼進 index.html 檔案中。
  6. 點擊上方 磁碟片圖示(儲存專案)
5

發布部署 & 回填 LINE Webhook 網址

約 2 分鐘完成

  1. 在 Apps Script 編輯器右上角,點擊藍色 部署 (Deploy) ➔ 新增部署作業 (New deployment)
  2. 點擊左側齒輪圖示 ➔ 選擇 網頁應用程式 (Web App)
    • 說明:輸入 v1
    • 執行身分:選擇 我 (自己的 Google 帳號)
    • 誰可以存取 (Who has access)務必選擇「所有人 (Anyone)」(這樣 LINE 伺服器才能把訊息傳進來!)。
  3. 按下 部署,若跳出授權視窗,點擊「核准存取權」➔「進階 (Advanced)」➔「前往專案(不安全)」➔「允許」。
  4. 部署完成後,複製畫面上顯示的 網頁應用程式網址 (Web App URL)
  5. 回到 LINE Developers ➔ Messaging API 頁籤
    • 找到 Webhook URL 欄位,貼上剛剛複製的網址並儲存。
    • 點擊 Verify(驗證) 按鈕,看到綠色 Success 即表示連線成功!
    • 確認 Use webhook 開關已切換為 開啟 (ON)
6

一鍵初始化資料庫 & 手機測試!

最後一步!即刻上線

  1. 回到 Apps Script 編輯器上方,將函式下拉選單切換為 initKnowledgeBaseSheet,點擊 執行 (Run)
  2. 執行完畢後切回 Google 試算表,您會發現試算表已經自動填滿 8 大核心領域的完整問答範例!
  3. 拿出手機開啟 LINE,掃描 LINE Developers 頁面上的 QR code 加入您自己的小幫手好友。
  4. 傳送一則訊息:「請問這節課是誰借平板車?」或「校園 Wi-Fi 怎麼設定?
  5. 🎉 恭喜!AI 智慧小幫手已經在 1 秒內連線資料庫親切又準確地回覆您囉!
Source Code Packages

專案核心檔案・一鍵複製打包中心

點擊下方右上角的「複製」按鈕,直接帶走完整、可立即運行的通用原始程式碼。

Code.gs (含平板借用系統 API 整合 / Gemini RAG)
/**
 * 竹光國中教務處暨資訊組 AI 智慧小幫手 (LINE Bot + Google Apps Script + Gemini RAG 後端)
 * 核心功能:
 * 1. Google 試算表 RAG 業務知識庫(零快取即時讀取,隨改隨生效)
 * 2. 教務處各組業務精確分流(註冊組、課研組、資訊組、教學組)
 * 3. 串接校園平板車與教室即時借用系統 (https://tablet-checkout.pages.dev/bookings)
 * 4. 串接竹光國中教務處 Google 行事曆 (即時查詢下週活動、段考、課發會、全校行事)
 * 5. 全中文數字與自然語言日期解析(支援「八月31日有平板車可以借嗎」、「下週活動」、「8/31 第二節誰借平板」等)
 * 6. 📝 設備線上報修登記系統(支援「#報修 地點與故障問題」自動寫入試算表)
 * 7. 🧠 未解問題自動收集機制(AI 未命中問題自動記錄至待補充問題庫)
 */

const CONFIG = {
  // 1. 填入您的 API 金鑰(請至 Google AI Studio 與 LINE Developers 取得)
  GEMINI_API_KEY: '填入您的_GEMINI_API_KEY',
  LINE_ACCESS_TOKEN: '填入您的_LINE_CHANNEL_ACCESS_TOKEN',
  
  // 2. 校園平板與空間即時借用系統 API
  BOOKING_API_URL: 'https://tablet-checkout.pages.dev/api/bookings',
  BOOKING_WEB_URL: 'https://tablet-checkout.pages.dev/bookings',

  // 3. 竹光國中教務處 Google 行事曆
  CALENDAR_ID: 'mail.zgjh.hc.edu.tw_ivesgumrre3jooutko74ts6ilg@group.calendar.google.com',
  CALENDAR_WEB_URL: 'https://calendar.google.com/calendar/embed?src=mail.zgjh.hc.edu.tw_ivesgumrre3jooutko74ts6ilg%40group.calendar.google.com&ctz=Asia%2FTaipei',

  // 4. 智慧小幫手專屬人設 (System Instruction)
  SYSTEM_INSTRUCTION: `你是一位專業、親切且有效率的「竹光國中資訊組暨教務處智慧小幫手」(資訊組長大乃老師的 AI 分身)。
你的任務是協助竹光國中全體教職員工快速解答校園業務、行事曆時程、各組執掌與設備操作。

【教務處各組業務執掌與分流原則(極重要)】:
1. 🏢【註冊組】:
   - 負責:1Campus「竹市校園智慧通」App、家長代碼發放與親子綁定、學生學籍與成績管理、段考補考業務 (註)。
2. 🏢【課研組】(課程與教學研發組):
   - 負責:作業檢查 / 作業抽查、課發會、班級共讀書箱、竹光書展週活動 (課)。
3. 🏢【資訊組】(大乃老師,合作樓2F資訊組,分機 614):
   - 負責:校園網路 (hc-school)、網芳 (120.104.14.3)、印表機 IP 設定、請假 VPN (FortiClient)、數位研習 (A1/A2/A3)、資安規範 (訊)。
   - 觸屏與大屏:ViewBoard 大屏故障排除、觸控線連接、無線投影 (Airsync/vCast)、大屏清潔保養。
   - 借用與報修:【平板車與專用教室線上借用系統】、設備線上報修 (#報修)。
4. 🏢【教學組】:
   - 負責:定期評量(段考)、九年級模擬考、課務排課、第八節輔導課、晚自修、寒暑假自習班 (教)。
5. 📅【教務處活動分流】:
   - 當回答行事曆時,依活動後方標註明確告知負責組別:(註)=註冊組、(課)=課研組、(訊)=資訊組、(教)=教學組。

【回答原則】:
1. 一律使用繁體中文(台灣習慣用語,如:影片、軟體、預設、網路、帳號、研習、平板車、網芳、大屏、段考、行事曆)。
2. 條理分明、結論先行,涉及操作或排查步驟時請用 1. 2. 3. 清楚列出。
3. 【嚴格 LINE 排版規範(極重要)】:
   - 嚴禁使用 Markdown 星號語法(絕對不要輸出 **粗體** 或 *斜體* 或 ### 或 - 破折號清單)!
   - 強調標題請使用【...】或「...」。
   - 清單一律使用 Emoji 圖示或數字(如 📱、🟢、🏫、⏰、📝、💡、👉、📅、🏢、•、1. 2. 3.)並適度空行,方便老師在手機 LINE 上舒適閱讀。
4. 【平板車與場地借用查詢規範(極重要)】:
   - 當老師詢問特定日期(例如「八月31日有平板車可以借嗎」)或特定節次時,請嚴格依據提示詞中提供的【⚡ 即時連線借用系統明細】回答。
   - 若老師問「某整天是否有車可借」(無指定節次),請告訴老師該日「哪些節次有空閒平板車可以借用(例如:第一節、第六節全數空閒;第二節有第九車、第十四車等)」以及已登記狀況。
   - 最後附上線上借用系統總覽網址:https://tablet-checkout.pages.dev/bookings
5. 【教務處行事曆與下週/本週活動查詢規範】:
   - 當老師詢問「下週活動」、「下週行事曆」、「這週活動」、「段考日程」等時,請嚴格依據提示詞中提供的【📅 竹光國中教務處行事曆即時時程明細】如實且詳細列出活動項目與日期。
   - 絕對不要回覆空泛或猜測的內容,請直接依據日曆清單條列!
   - 最後附上教務處線上日曆網址:https://calendar.google.com/calendar/embed?src=mail.zgjh.hc.edu.tw_ivesgumrre3jooutko74ts6ilg%40group.calendar.google.com&ctz=Asia%2FTaipei
6. 若涉及程式下載或教學影片,請依規定附上直接下載之英文網址。
7. 若知識庫查無特定資訊,請委婉告知老師並依上述分流引導至對應組別。
8. 保持熱心、溫暖且支持的語氣。`,

  // 5. 管理後台與手機寫入密碼(留空即為免密碼模式)
  ADMIN_PASSWORD: '',
  
  // 6. Google 試算表工作表名稱
  SHEET_NAME: '資訊組知識庫',
  LOG_SHEET_NAME: '對話紀錄',
  REPAIR_SHEET_NAME: '報修登記表',
  UNANSWERED_SHEET_NAME: '待補充問題庫'
};

// ==========================================
// 節次時間表(台灣時間 UTC+8)
// ==========================================
const PERIOD_TIMES = [
  { name: '早修', start: '07:30', end: '08:25' },
  { name: '第一節', start: '08:25', end: '09:15' },
  { name: '第二節', start: '09:15', end: '10:10' },
  { name: '第三節', start: '10:10', end: '11:05' },
  { name: '第四節', start: '11:05', end: '12:00' },
  { name: '午休', start: '12:00', end: '13:10' },
  { name: '第五節', start: '13:10', end: '14:00' },
  { name: '第六節', start: '14:00', end: '14:55' },
  { name: '第七節', start: '14:55', end: '15:50' },
  { name: '第八節', start: '15:50', end: '16:55' },
  { name: '夜間', start: '16:55', end: '21:00' }
];

const TEACHING_PERIODS = ['第一節', '第二節', '第三節', '第四節', '第五節', '第六節', '第七節'];
const CARTS_ALL = ["第九車", "第十車", "第十一車", "第十二車", "第十四車", "第十五車"];

// ==========================================
// Web 介面與 Webhook 入口
// ==========================================

function doGet(e) {
  return HtmlService.createHtmlOutputFromFile('index')
    .setTitle('竹光國中資訊組 AI 智慧小幫手 - 控制台')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
    .addMetaTag('viewport', 'width=device-width, initial-scale=1');
}

function doPost(e) {
  try {
    if (!e || !e.postData || !e.postData.contents) {
      return ContentService.createTextOutput('OK').setMimeType(ContentService.MimeType.TEXT);
    }
    const data = JSON.parse(e.postData.contents);
    if (!data.events || data.events.length === 0) {
      return ContentService.createTextOutput('OK').setMimeType(ContentService.MimeType.TEXT);
    }

    const event = data.events[0];
    if (event.type === 'message' && event.message.type === 'text') {
      const userMessage = event.message.text.trim();
      const replyToken = event.replyToken;
      const isGroup = event.source.type === 'group' || event.source.type === 'room';
      const userId = event.source.userId || 'unknown';

      // 1. 立即啟動 LINE「正在輸入中...」載入動畫
      if (event.source.userId) {
        showLineLoading(event.source.userId);
      }

      // 群組模式過濾:若在群組中,只有 Tag @小幫手 或提及關鍵字才回應
      if (isGroup) {
        const botTrigger = /(@小幫手|資訊小幫手|資訊組長|大乃老師|小幫手)/i;
        if (!botTrigger.test(userMessage) && !userMessage.startsWith('#')) {
          return ContentService.createTextOutput('OK');
        }
      }

      // 2. 檢查是否為設備報修指令 (#報修 ...)
      if (userMessage.startsWith('#報修') || userMessage.startsWith('報修 ')) {
        const repairReply = handleRepairCommand(userMessage, userId);
        sendLineReply(replyToken, repairReply);
        logConversation(userMessage, repairReply, userId);
        return ContentService.createTextOutput('OK');
      }

      // 3. 檢查是否為管理員指令 (#記住, #查詢, #查詢報修)
      if (userMessage.startsWith('#')) {
        const adminReply = handleAdminCommand(userMessage);
        sendLineReply(replyToken, adminReply);
        return ContentService.createTextOutput('OK');
      }

      // 4. 一般使用者提問:執行 RAG 知識庫比對 + 即時借用 API + 行事曆 API 與 AI 回答
      const cleanQuery = userMessage.replace(/(@小幫手|資訊小幫手|資訊組長|大乃老師|小幫手)/gi, '').trim();
      const aiReply = generateAiResponse(cleanQuery, userId);
      
      sendLineReply(replyToken, aiReply);
      logConversation(cleanQuery, aiReply, userId);
    }

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

// ==========================================
// 📝 設備線上報修登記系統
// ==========================================

function handleRepairCommand(text, userId) {
  const content = text.replace(/^(#報修|報修)\s*/, '').trim();
  if (!content) {
    return '🛠️ 【資訊組設備線上報修格式說明】\n\n' +
      '請輸入:#報修 [地點/教室] [設備名稱與故障現象]\n\n' +
      '👉 範例:\n' +
      '• #報修 804班大屏HDMI孔接觸不良\n' +
      '• #報修 電腦教室3投影機燈泡故障閃爍\n' +
      '• #報修 專任辦公室Sharp印表機卡紙\n\n' +
      '💡 登記後大乃老師將即時收到報修紀錄並前往排查!';
  }

  const now = new Date();
  const twTimeStr = Utilities.formatDate(now, 'GMT+8', 'yyyy/MM/dd HH:mm');
  const repairId = 'R' + Utilities.formatDate(now, 'GMT+8', 'MMddHHmm');

  try {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    let sheet = ss.getSheetByName(CONFIG.REPAIR_SHEET_NAME);
    if (!sheet) {
      sheet = ss.insertSheet(CONFIG.REPAIR_SHEET_NAME);
      sheet.appendRow(['報修單號', '登記時間', '報修者ID', '報修地點與故障內容', '處理狀態', '處理備註', '完成時間']);
      sheet.getRange('A1:G1').setBackground('#b91c1c').setFontColor('#ffffff').setFontWeight('bold');
    }

    sheet.appendRow([repairId, twTimeStr, userId, content, '⏳ 待處理', '', '']);
    
    return `🛠️ 【竹光資訊組・設備報修已受理】\n\n` +
      `• 報修單號:${repairId}\n` +
      `• 登記時間:${twTimeStr}\n` +
      `• 故障描述:${content}\n` +
      `• 目前狀態:⏳ 待處理\n\n` +
      `大乃老師已收到您的報修登記,資訊組將盡速前往檢修排查!\n` +
      `💡 若為上課緊急教學需求,請撥打校內分機 614。`;
  } catch (e) {
    console.error('handleRepairCommand error:', e);
    return `❌ 報修登記寫入失敗,請直接撥打分機 614 聯絡資訊組大乃老師!`;
  }
}

// ==========================================
// 🧠 未解問題自動收集機制 (知識庫自進化)
// ==========================================

function logUnansweredQuestion(query, reply, userId) {
  try {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    let sheet = ss.getSheetByName(CONFIG.UNANSWERED_SHEET_NAME);
    if (!sheet) {
      sheet = ss.insertSheet(CONFIG.UNANSWERED_SHEET_NAME);
      sheet.appendRow(['記錄時間', '提問者ID', '未命中之提問問題', 'AI當次回覆', '處理狀態', '大乃老師補充解答']);
      sheet.getRange('A1:F1').setBackground('#c2410c').setFontColor('#ffffff').setFontWeight('bold');
    }

    const now = new Date();
    const twTimeStr = Utilities.formatDate(now, 'GMT+8', 'yyyy/MM/dd HH:mm:ss');
    sheet.appendRow([twTimeStr, userId, query, reply.slice(0, 100), '🔍 待補充', '']);
  } catch (e) {
    console.error('logUnansweredQuestion error:', e);
  }
}

// ==========================================
// 📅 教務處 Google 行事曆即時查詢 (高精度解析)
// ==========================================

function getSchoolCalendarSummary(query) {
  try {
    const now = new Date();
    const twTime = new Date(now.getTime() + (8 * 3600 * 1000) + (now.getTimezoneOffset() * 60000));
    const normQuery = normalizeChineseDate(query);
    
    let startY = twTime.getFullYear();
    let startM = twTime.getMonth();
    let startD = twTime.getDate();
    
    let startDate = new Date(startY, startM, startD, 0, 0, 0);
    let endDate = new Date(startY, startM + 3, 1, 23, 59, 59);

    const curDayOfWeek = twTime.getDay();

    if (normQuery.includes('下週') || normQuery.includes('下禮拜') || normQuery.includes('下星期')) {
      const daysUntilNextMon = (curDayOfWeek === 0 ? 1 : 8 - curDayOfWeek);
      startDate = new Date(twTime.getTime() + daysUntilNextMon * 86400000);
      startDate.setHours(0, 0, 0, 0);
      endDate = new Date(startDate.getTime() + 6 * 86400000);
      endDate.setHours(23, 59, 59, 999);
    } else if (normQuery.includes('這週') || normQuery.includes('本週') || normQuery.includes('這禮拜') || normQuery.includes('這星期')) {
      const daysFromMon = (curDayOfWeek === 0 ? 6 : curDayOfWeek - 1);
      startDate = new Date(twTime.getTime() - daysFromMon * 86400000);
      startDate.setHours(0, 0, 0, 0);
      endDate = new Date(startDate.getTime() + 6 * 86400000);
      endDate.setHours(23, 59, 59, 999);
    } else if (normQuery.includes('明天')) {
      startDate = new Date(twTime.getTime() + 86400000);
      startDate.setHours(0, 0, 0, 0);
      endDate = new Date(startDate.getTime());
      endDate.setHours(23, 59, 59, 999);
    } else if (normQuery.includes('今天') || normQuery.includes('今日')) {
      startDate.setHours(0, 0, 0, 0);
      endDate = new Date(startDate.getTime());
      endDate.setHours(23, 59, 59, 999);
    } else {
      const mMonth = normQuery.match(/(\d{1,2})月/);
      if (mMonth) {
        const targetM = parseInt(mMonth[1], 10) - 1;
        let targetY = twTime.getFullYear();
        if (targetM < twTime.getMonth()) targetY += 1;
        startDate = new Date(targetY, targetM, 1, 0, 0, 0);
        endDate = new Date(targetY, targetM + 1, 0, 23, 59, 59);
      }
    }

    const startStr = formatDateStr(startDate);
    const endStr = formatDateStr(endDate);

    const cache = CacheService.getScriptCache();
    let icsText = cache.get('ZGJH_CAL_ICS_V2');
    if (!icsText) {
      const icsUrl = 'https://calendar.google.com/calendar/ical/' + encodeURIComponent(CONFIG.CALENDAR_ID) + '/public/basic.ics';
      const resp = UrlFetchApp.fetch(icsUrl, { muteHttpExceptions: true });
      if (resp.getResponseCode() === 200) {
        icsText = resp.getContentText();
        try { cache.put('ZGJH_CAL_ICS_V2', icsText, 3600); } catch(e) {}
      }
    }

    let eventList = [];
    if (icsText) {
      const vEvents = icsText.split('BEGIN:VEVENT');
      for (let i = 1; i < vEvents.length; i++) {
        const block = vEvents[i];
        const summaryMatch = block.match(/SUMMARY:(.*?)(?:\r?\n|$)/);
        const dtStartMatch = block.match(/DTSTART[^:]*:(\d{4})(\d{2})(\d{2})/);
        
        if (summaryMatch && dtStartMatch) {
          const sum = summaryMatch[1].trim();
          const y = parseInt(dtStartMatch[1], 10);
          const m = parseInt(dtStartMatch[2], 10) - 1;
          const d = parseInt(dtStartMatch[3], 10);
          const evDate = new Date(y, m, d, 0, 0, 0);

          if (evDate >= startDate && evDate <= endDate) {
            const dateFmt = `${y}-${String(m+1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
            const weekDayMap = ['日', '一', '二', '三', '四', '五', '六'];
            const wStr = weekDayMap[evDate.getDay()];
            
            // 解析所屬組別
            let dept = '';
            if (sum.includes('(註)') || sum.includes('(註)')) dept = '【註冊組】';
            else if (sum.includes('(課)') || sum.includes('(課)')) dept = '【課研組】';
            else if (sum.includes('(訊)') || sum.includes('(訊)')) dept = '【資訊組】';
            else if (sum.includes('(教)') || sum.includes('(教)')) dept = '【教學組】';

            eventList.push({
              dateStr: dateFmt,
              display: `• 【${String(m+1).padStart(2, '0')}/${String(d).padStart(2, '0')} (${wStr})】${dept}:${sum}`
            });
          }
        }
      }
    }

    eventList.sort((a, b) => a.dateStr.localeCompare(b.dateStr));

    if (eventList.length === 0) {
      return `【教務處行事曆】:在 ${startStr} 至 ${endStr} 期間查無特定登記活動(可至線上日曆 ${CONFIG.CALENDAR_WEB_URL} 查看)。`;
    }

    return `📅 【竹光國中教務處行事曆即時明細 (${startStr} ~ ${endStr},共 ${eventList.length} 項)】:\n` +
      eventList.map(e => e.display).join('\n') +
      `\n\n【教務處完整線上行事曆】:${CONFIG.CALENDAR_WEB_URL}`;
  } catch(e) {
    console.error('getSchoolCalendarSummary error:', e);
    return `【教務處行事曆】:可至線上日曆查看:${CONFIG.CALENDAR_WEB_URL}`;
  }
}

// ==========================================
// 知識庫管理核心函式 (零快取即時讀取)
// ==========================================

function getKnowledgeBaseData() {
  try {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    let sheet = ss.getSheetByName(CONFIG.SHEET_NAME);
    if (!sheet) {
      initKnowledgeBaseSheet();
      sheet = ss.getSheetByName(CONFIG.SHEET_NAME);
    }
    const values = sheet.getDataRange().getValues();
    if (values.length <= 1) return [];

    const list = [];
    for (let i = 1; i < values.length; i++) {
      const row = values[i];
      const cat = (row[0] || '').toString().trim();
      const title = (row[1] || '').toString().trim();
      const content = (row[2] || '').toString().trim();
      const status = (row[3] || '').toString().trim();

      if ((cat || title) && content && status !== '停用' && status !== 'false') {
        list.push({
          category: cat || '一般校務業務',
          title: title || cat,
          content: content
        });
      }
    }

    if (list.length > 0) return list;
  } catch (e) {
    console.warn('getKnowledgeBaseData error:', e);
  }

  return [];
}

function handleAdminCommand(text) {
  const parts = text.split(' ');
  const cmd = parts[0];
  
  if (cmd === '#查詢報修') {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const sheet = ss.getSheetByName(CONFIG.REPAIR_SHEET_NAME);
    if (!sheet) return '📋 目前尚無任何報修登記紀錄!';
    const values = sheet.getDataRange().getValues();
    if (values.length <= 1) return '📋 目前尚無任何報修登記紀錄!';
    
    let pending = [];
    for (let i = 1; i < values.length; i++) {
      if (values[i][4] === '⏳ 待處理' || values[i][4] === '待處理') {
        pending.push(`• [${values[i][0]}] ${values[i][1]}:${values[i][3]}`);
      }
    }
    if (pending.length === 0) return '🎉 太棒了!目前所有報修皆已處理完成,無待辦工單!';
    return `🛠️ 【待處理報修清單 (${pending.length} 筆)】:\n` + pending.slice(0, 8).join('\n');
  }

  if (cmd === '#查詢') {
    const kw = parts.slice(1).join(' ');
    const list = getKnowledgeBaseData().filter(k => k.title.includes(kw) || k.content.includes(kw) || k.category.includes(kw));
    if (list.length === 0) return `🔍 查無包含「${kw}」的資訊組項目。`;
    return `🔍 找到 ${list.length} 筆資料:\n` + list.map(x => `• [${x.category}] ${x.title}:${x.content.slice(0, 40)}...`).join('\n');
  }
  
  if (cmd === '#記住') {
    let content = parts.slice(1).join(' ');
    if (CONFIG.ADMIN_PASSWORD && parts[1] === CONFIG.ADMIN_PASSWORD) {
      content = parts.slice(2).join(' ');
    } else if (CONFIG.ADMIN_PASSWORD) {
      return '❌ 密碼錯誤,無法寫入知識庫。';
    }
    
    if (!content) return '❌ 請提供要記住的內容,例如:#記住 電腦教室2的投影機燈泡已更換';
    
    const extracted = extractCategoryAndTitle(content);
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const sheet = ss.getSheetByName(CONFIG.SHEET_NAME);
    sheet.appendRow([extracted.category, extracted.title, content, '啟用', new Date()]);
    return `✅ 已成功寫入校園資訊知識庫!\n【分類】${extracted.category}\n【項目】${extracted.title}\n【內容】${content}`;
  }

  return '💡 支援指令:\n• #報修 [地點與問題]\n• #查詢報修\n• #查詢 [關鍵字]\n• #記住 [內容]';
}

function extractCategoryAndTitle(text) {
  return { category: '一般資訊業務', title: text.slice(0, 15) };
}

// ==========================================
// 中文數字與自然語言日期正規化函式
// ==========================================

function normalizeChineseDate(text) {
  if (!text) return '';
  let str = text;
  
  const cnNums = [
    { cn: '三十一', num: '31' }, { cn: '三十', num: '30' },
    { cn: '二十九', num: '29' }, { cn: '二十八', num: '28' },
    { cn: '二十七', num: '27' }, { cn: '二十六', num: '26' },
    { cn: '二十五', num: '25' }, { cn: '二十四', num: '24' },
    { cn: '二十三', num: '23' }, { cn: '二十二', num: '22' },
    { cn: '二十一', num: '21' }, { cn: '二十', num: '20' },
    { cn: '十九', num: '19' }, { cn: '十八', num: '18' },
    { cn: '十七', num: '17' }, { cn: '十六', num: '16' },
    { cn: '十五', num: '15' }, { cn: '十四', num: '14' },
    { cn: '十三', num: '13' }, { cn: '十二', num: '12' },
    { cn: '十一', num: '11' }, { cn: '十', num: '10' },
    { cn: '九', num: '9' }, { cn: '八', num: '8' },
    { cn: '七', num: '7' }, { cn: '六', num: '6' },
    { cn: '五', num: '5' }, { cn: '四', num: '4' },
    { cn: '三', num: '3' }, { cn: '二', num: '2' },
    { cn: '一', num: '1' }
  ];

  for (let i = 0; i < cnNums.length; i++) {
    const item = cnNums[i];
    str = str.split(item.cn + '月').join(item.num + '月');
    str = str.split(item.cn + '日').join(item.num + '日');
    str = str.split(item.cn + '號').join(item.num + '號');
    str = str.split(item.cn + '號').join(item.num + '號');
  }

  return str;
}

// ==========================================
// 智慧日期與節次解析
// ==========================================

function parseTargetDate(query, twTime) {
  const norm = normalizeChineseDate(query);
  const year = twTime.getFullYear();
  
  // 1. 完整年月日 (2026/8/31, 2026-08-31, 2026年8月31日)
  const mFull = norm.match(/(\d{4})[\/\-年.](\d{1,2})[\/\-月.](\d{1,2})/);
  if (mFull) {
    const y = mFull[1];
    const m = String(mFull[2]).padStart(2, '0');
    const d = String(mFull[3]).padStart(2, '0');
    return `${y}-${m}-${d}`;
  }

  // 2. 月日 (8/31, 08/31, 8-31, 8月31日, 8月31號, 8月31, 八月31日)
  const mMd = norm.match(/(\d{1,2})[\/\-月](\d{1,2})[日號]?/);
  if (mMd) {
    const m = String(mMd[1]).padStart(2, '0');
    const d = String(mMd[2]).padStart(2, '0');
    return `${year}-${m}-${d}`;
  }

  // 3. 相對日期
  if (norm.includes('大後天')) {
    const d = new Date(twTime.getTime() + 3 * 86400000);
    return formatDateStr(d);
  } else if (norm.includes('後天')) {
    const d = new Date(twTime.getTime() + 2 * 86400000);
    return formatDateStr(d);
  } else if (norm.includes('明天')) {
    const d = new Date(twTime.getTime() + 1 * 86400000);
    return formatDateStr(d);
  } else if (norm.includes('昨天')) {
    const d = new Date(twTime.getTime() - 1 * 86400000);
    return formatDateStr(d);
  }

  // 4. 星期幾 / 下週幾
  const wMap = { '一': 1, '二': 2, '三': 3, '四': 4, '五': 5, '六': 6, '日': 0, '天': 0 };
  const mWeek = norm.match(/(下週|下禮拜|下星期|這週|這禮拜|這星期|週|禮拜|星期)([一二三四五六日天])/);
  if (mWeek) {
    const prefix = mWeek[1];
    const targetW = wMap[mWeek[2]];
    const curW = twTime.getDay();
    let diff = (targetW - curW + 7) % 7;
    if (prefix.startsWith('下')) {
      diff += 7;
    } else if (diff === 0 && !prefix.startsWith('這')) {
      diff = 7;
    }
    const d = new Date(twTime.getTime() + diff * 86400000);
    return formatDateStr(d);
  }

  return formatDateStr(twTime);
}

function formatDateStr(d) {
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2, '0');
  const day = String(d.getDate()).padStart(2, '0');
  return `${y}-${m}-${day}`;
}

// ==========================================
// 即時平板車與場地借用系統資料抓取與結構化
// ==========================================

function getLiveBookingSummary(query) {
  try {
    const now = new Date();
    const twTime = new Date(now.getTime() + (8 * 3600 * 1000) + (now.getTimezoneOffset() * 60000));
    const targetDate = parseTargetDate(query, twTime);
    const todayStr = formatDateStr(twTime);
    const timeStr = Utilities.formatDate(twTime, 'GMT+8', 'HH:mm');

    const normQuery = normalizeChineseDate(query);

    let queryPeriod = null;
    if (normQuery.includes('早修')) queryPeriod = '早修';
    else if (normQuery.match(/(第一節|第1節|一節)/)) queryPeriod = '第一節';
    else if (normQuery.match(/(第二節|第2節|二節)/)) queryPeriod = '第二節';
    else if (normQuery.match(/(第三節|第3節|三節)/)) queryPeriod = '第三節';
    else if (normQuery.match(/(第四節|第4節|四節)/)) queryPeriod = '第四節';
    else if (normQuery.includes('午休')) queryPeriod = '午休';
    else if (normQuery.match(/(第五節|第5節|五節)/)) queryPeriod = '第五節';
    else if (normQuery.match(/(第六節|第6節|六節)/)) queryPeriod = '第六節';
    else if (normQuery.match(/(第七節|第7節|七節)/)) queryPeriod = '第七節';
    else if (normQuery.match(/(第八節|第8節|八節)/)) queryPeriod = '第八節';
    else if (normQuery.includes('夜間')) queryPeriod = '夜間';

    let currentPeriod = '非上課時段 (課後或休息時間)';
    const totalMinutes = twTime.getHours() * 60 + twTime.getMinutes();
    for (let p of PERIOD_TIMES) {
      const [sh, sm] = p.start.split(':').map(Number);
      const [eh, em] = p.end.split(':').map(Number);
      if (totalMinutes >= sh * 60 + sm && totalMinutes < eh * 60 + em) {
        currentPeriod = p.name;
        break;
      }
    }

    const effectivePeriod = queryPeriod || ((targetDate === todayStr && (normQuery.includes('這節') || normQuery.includes('現在'))) ? currentPeriod : null);

    const apiUrl = `${CONFIG.BOOKING_API_URL}?day=${targetDate}`;
    const resp = UrlFetchApp.fetch(apiUrl, { muteHttpExceptions: true });
    
    if (resp.getResponseCode() !== 200) {
      return `【查詢日期】:${targetDate}(線上借用系統暫時無回應,可至 ${CONFIG.BOOKING_WEB_URL} 查看)`;
    }

    const data = JSON.parse(resp.getContentText());
    const tablets = data.tablets || [];
    const rooms = data.rooms || [];

    let summary = `【查詢目標日期】:${targetDate}\n`;
    if (targetDate === todayStr) {
      summary += `【目前台灣時間】:${timeStr}(當前所屬節次:${currentPeriod})\n`;
    }
    summary += `【所有列管平板車】:${CARTS_ALL.join('、')}\n\n`;

    // 1. 若有指定節次,進行專屬分析
    if (effectivePeriod && effectivePeriod !== '非上課時段 (課後或休息時間)') {
      summary += `【查詢指定節次】:${effectivePeriod}\n`;
      const periodBookedTablets = tablets.filter(t => t.periods && t.periods.includes(effectivePeriod));
      const bookedNames = periodBookedTablets.map(t => t.item);
      const freeCarts = CARTS_ALL.filter(c => !bookedNames.includes(c));

      summary += `⚡ 【${targetDate} ${effectivePeriod} 平板車即時借用狀態】:\n`;
      if (periodBookedTablets.length === 0) {
        summary += `• 該節課【全部平板車】皆為空閒,無人借用!\n`;
      } else {
        periodBookedTablets.forEach(t => {
          summary += `• 📱 【${t.item}】:已由「${t.teacher}」借用${t.longterm ? '【長期借用】' : ''}${t.msg ? '(用途:' + t.msg + ')' : ''}\n`;
        });
      }
      summary += `• 🟢 【${effectivePeriod} 目前空閒可借用平板車】:${freeCarts.length > 0 ? freeCarts.join('、') : '本節無空閒平板車'}\n\n`;

      const periodRooms = rooms.filter(r => r.periods && r.periods.includes(effectivePeriod));
      summary += `🏫 【${targetDate} ${effectivePeriod} 教室/場地借用狀況】:\n`;
      if (periodRooms.length === 0) {
        summary += `• 該節無特殊教室/場地借用登記。\n`;
      } else {
        periodRooms.forEach(r => {
          summary += `• 【${r.item}】:借用教師「${r.teacher}」${r.longterm ? '【長期借用】' : ''}${r.msg ? '(事由:' + r.msg + ')' : ''}\n`;
        });
      }
      summary += '\n';
    } else {
      // 2. 未指定節次(問全天):自動彙整各節次空閒與借用情況
      summary += `⚡ 【${targetDate} 全天上課各節次平板車即時借用概況】:\n`;
      TEACHING_PERIODS.forEach(p => {
        const pBooked = tablets.filter(t => t.periods && t.periods.includes(p));
        const bNames = pBooked.map(t => t.item);
        const fCarts = CARTS_ALL.filter(c => !bNames.includes(c));
        if (pBooked.length === 0) {
          summary += `• 【${p}】:🟢 全車可借(6台全空閒)\n`;
        } else {
          summary += `• 【${p}】:已借「${bNames.join('、')}」;🟢 可借「${fCarts.length > 0 ? fCarts.join('、') : '無'}」\n`;
        }
      });
      summary += '\n';
    }

    // 3. 全天完整明細清單
    summary += `📋 【${targetDate} 全天平板車登記總明細 (${tablets.length} 筆)】:\n`;
    if (tablets.length === 0) {
      summary += `• 該日全天無任何平板車借用記錄。\n`;
    } else {
      tablets.forEach(t => {
        summary += `• 【${t.item}】:教師「${t.teacher}」,登記節次:${t.periods.join('、')}${t.longterm ? '【長期借用】' : ''}${t.msg ? '(用途:' + t.msg + ')' : ''}\n`;
      });
    }

    summary += `\n🏫 【${targetDate} 全天教室/場地借用總明細 (${rooms.length} 筆)】:\n`;
    if (rooms.length === 0) {
      summary += `• 該日無場地/教室借用記錄。\n`;
    } else {
      rooms.forEach(r => {
        summary += `• 【${r.item}】:教師「${r.teacher}」,登記節次:${r.periods.join('、')}${r.longterm ? '【長期借用】' : ''}${r.msg ? '(事由:' + r.msg + ')' : ''}\n`;
      });
    }

    summary += `\n【即時借用登記系統網址】:${CONFIG.BOOKING_WEB_URL}`;
    return summary;
  } catch (e) {
    console.error('getLiveBookingSummary error:', e);
    return '(即時借用系統連線異常)';
  }
}

// ==========================================
// RAG 知識庫與 Gemini AI 生成 (Google ModelService 動態直連)
// ==========================================

function generateAiResponse(userQuery, userId) {
  if (!CONFIG.GEMINI_API_KEY || CONFIG.GEMINI_API_KEY.includes('填入您的')) {
    return '⚠️ 系統提示:尚未在 Code.gs 第 11 行設定 GEMINI_API_KEY,請先前往 Google AI Studio 取得免費金鑰!';
  }

  const kbData = getKnowledgeBaseData();
  const contextText = kbData.map(function(item) {
    return '【分類:' + item.category + '】項目:' + item.title + '\n內容:' + item.content;
  }).join('\n\n');

  const normQuery = normalizeChineseDate(userQuery);

  // 1. 判斷是否詢問平板或空間借用
  let liveBookingContext = '';
  const isBookingQuery = /(借|平板|車|這節|誰借|第.節|幾號車|卓越基地|電腦教室|沙特|盧梭|黑格爾|未來基地|階梯教室|教室借用|登記|預約|預定|\d{1,2}\/\d{1,2}|\d{1,2}月\d{1,2})/i.test(normQuery);
  if (isBookingQuery) {
    liveBookingContext = getLiveBookingSummary(userQuery);
  }

  // 2. 判斷是否詢問教務處行事曆、下週活動、段考、考試
  let liveCalendarContext = '';
  const isCalendarQuery = /(行事曆|段考|考試|月考|開學|放假|行事|活動|時程|日程|週會|朝會|返校|結業式|補課|課發會|科展|自習|暑輔|寒輔|定期評量|下週|這週|本週|下禮拜|這禮拜|明天|今天)/i.test(normQuery);
  if (isCalendarQuery) {
    liveCalendarContext = getSchoolCalendarSummary(userQuery);
  }

  const prompt = CONFIG.SYSTEM_INSTRUCTION + '\n\n' +
    '以下是「竹光國中資訊組與教務處業務知識庫」:\n' +
    '--------------------------------\n' +
    contextText + '\n' +
    '--------------------------------\n' +
    (liveBookingContext ? ('\n【⚡ 即時連線:竹光國中平板車與教室借用系統即時資料庫 (https://tablet-checkout.pages.dev/bookings)】\n' + liveBookingContext + '\n--------------------------------\n') : '') +
    (liveCalendarContext ? ('\n【📅 即時連線:竹光國中教務處 Google 行事曆 (https://calendar.google.com/calendar/embed?src=mail.zgjh.hc.edu.tw_ivesgumrre3jooutko74ts6ilg%40group.calendar.google.com&ctz=Asia%2FTaipei)】\n' + liveCalendarContext + '\n--------------------------------\n') : '') +
    '\n老師/同仁的提問是:「' + userQuery + '」\n\n' +
    '請特別注意:\n' +
    '1. 若提問涉及平板車或空間借用,請務必依據上方【⚡ 即時連線:竹光國中平板車與教室借用系統即時資料庫】提供的查詢目標日期與節次狀態精確回答!\n' +
    '2. 若老師詢問某一天是否有平板車可借(如「八月31日有平板車可以借嗎」),請直接告訴老師該日有哪些節次有空閒車輛可供登記借用,並條理清楚呈現。\n' +
    '3. 依據問題內容對應教務處正確的組別(註冊組、課研組、資訊組、教學組)進行回答與窗口指引。\n\n' +
    '請依據上述知識庫、即時借用明細與教務處行事曆,提供完整、條理分明且親切的回答:';

  const payload = {
    contents: [{ parts: [{ text: prompt }] }],
    generationConfig: {
      temperature: 0.2
    }
  };

  // 動態向 Google 伺服器抓取您金鑰授權的所有模型
  const models = getAuthorizedGoogleModels(CONFIG.GEMINI_API_KEY);
  if (models.length === 0) {
    return '⚠️ Google API 金鑰異常:無法從 Google 取得可用模型,請確認 API Key 是否在 Google AI Studio 正常啟用!';
  }

  let lastError = '';
  for (let i = 0; i < models.length; i++) {
    const model = models[i];
    try {
      const url = 'https://generativelanguage.googleapis.com/v1beta/models/' + model + ':generateContent?key=' + CONFIG.GEMINI_API_KEY;
      const response = UrlFetchApp.fetch(url, {
        method: 'post',
        contentType: 'application/json',
        payload: JSON.stringify(payload),
        muteHttpExceptions: true
      });
      
      const resJson = JSON.parse(response.getContentText());
      if (resJson.candidates && resJson.candidates[0] && resJson.candidates[0].content && resJson.candidates[0].content.parts && resJson.candidates[0].content.parts[0].text) {
        const finalReply = sanitizeLineFormat(resJson.candidates[0].content.parts[0].text);
        
        // 🧠 檢查是否為知識庫未命中的問題,若是則自動收集進待補充清單
        if (finalReply.includes('小幫手目前沒有這項特定資料') || finalReply.includes('合作樓2樓教務處資訊組')) {
          logUnansweredQuestion(userQuery, finalReply, userId || 'anonymous');
        }
        
        return finalReply;
      } else if (resJson.error) {
        lastError = '[' + model + '] ' + (resJson.error.message || JSON.stringify(resJson.error));
      }
    } catch (e) {
      lastError = '[' + model + '] 連線失敗: ' + e.toString();
    }
  }

  if (lastError.indexOf('RESOURCE_EXHAUSTED') !== -1 || lastError.indexOf('429') !== -1) {
    return '⚠️ 系統提示:目前連線頻率較高,請稍候 3 秒後再次提問!';
  }

  return '⚠️ Gemini API 錯誤: ' + (lastError || '連線繁忙,請稍候再試');
}

function getAuthorizedGoogleModels(apiKey) {
  const cache = CacheService.getScriptCache();
  const cached = cache.get('AUTHORIZED_GEMINI_MODELS_V6');
  if (cached) {
    try { return JSON.parse(cached); } catch(e) {}
  }

  try {
    const url = 'https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey;
    const resp = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
    const data = JSON.parse(resp.getContentText());
    if (data.models && data.models.length > 0) {
      const valid = [];
      for (let i = 0; i < data.models.length; i++) {
        const m = data.models[i];
        if (m.supportedGenerationMethods && m.supportedGenerationMethods.indexOf('generateContent') !== -1) {
          valid.push(m.name.replace('models/', ''));
        }
      }
      
      // 排序:優先使用 flash 系列,次之 pro
      valid.sort(function(a, b) {
        if (a.indexOf('flash') !== -1 && b.indexOf('flash') === -1) return -1;
        if (a.indexOf('flash') === -1 && b.indexOf('flash') !== -1) return 1;
        return 0;
      });

      if (valid.length > 0) {
        cache.put('AUTHORIZED_GEMINI_MODELS_V6', JSON.stringify(valid), 86400);
        return valid;
      }
    }
  } catch (e) {
    console.warn('getAuthorizedGoogleModels error:', e);
  }

  return ['gemini-1.5-flash-latest', 'gemini-1.5-flash', 'gemini-2.5-flash'];
}

function sendLineReply(replyToken, messageText) {
  const url = 'https://api.line.me/v2/bot/message/reply';
  UrlFetchApp.fetch(url, {
    method: 'post',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + CONFIG.LINE_ACCESS_TOKEN
    },
    payload: JSON.stringify({
      replyToken: replyToken,
      messages: [{ type: 'text', text: messageText }]
    }),
    muteHttpExceptions: true
  });
}

function logConversation(q, a, user) {
  try {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    let sheet = ss.getSheetByName(CONFIG.LOG_SHEET_NAME);
    if (!sheet) {
      sheet = ss.insertSheet(CONFIG.LOG_SHEET_NAME);
      sheet.appendRow(['時間', '提問者ID', '提問內容', 'AI回覆']);
      sheet.getRange('A1:D1').setBackground('#0f766e').setFontColor('#ffffff').setFontWeight('bold');
    }
    sheet.appendRow([new Date(), user, q, a]);
  } catch (e) {}
}

function sanitizeLineFormat(text) {
  if (!text) return '';
  return text
    .replace(/\*\*(.*?)\*\*/g, '【$1】')
    .replace(/^\s*[\*\-]\s*([📱🟢🏫⏰📝💡👉📅🏢•【「])/gm, '$1')
    .replace(/^\s*[\*\-]\s+/gm, '• ')
    .replace(/\*(.*?)\*/g, '$1')
    .replace(/\n{3,}/g, '\n\n')
    .trim();
}

function showLineLoading(chatId) {
  if (!chatId || !CONFIG.LINE_ACCESS_TOKEN) return;
  try {
    UrlFetchApp.fetch('https://api.line.me/v2/bot/chat/loading/start', {
      method: 'post',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + CONFIG.LINE_ACCESS_TOKEN
      },
      payload: JSON.stringify({
        chatId: chatId,
        loadingSeconds: 20
      }),
      muteHttpExceptions: true
    });
  } catch (e) {}
}

function testGeminiApi() {
  console.log('--- 開始執行 Gemini 小幫手後端自我診斷測試 ---');
  console.log('1. 檢查 GEMINI_API_KEY 是否填寫: ' + (CONFIG.GEMINI_API_KEY ? '已填寫 (前6碼: ' + CONFIG.GEMINI_API_KEY.slice(0, 6) + '...)' : '❌ 未填寫!'));
  console.log('2. 檢查 LINE_ACCESS_TOKEN 是否填寫: ' + (CONFIG.LINE_ACCESS_TOKEN ? '已填寫 (長度: ' + CONFIG.LINE_ACCESS_TOKEN.length + ')' : '❌ 未填寫!'));
  
  console.log('3. 測試中文月份日期解析(八月31日有平板車可以借嗎)...');
  const bookingSummary = getLiveBookingSummary('八月31日有平板車可以借嗎?');
  console.log('即時借用回傳結果預覽:\n' + bookingSummary);

  console.log('4. 測試 Gemini AI 回覆八月31日借車問答...');
  const reply = generateAiResponse('八月31日有平板車可以借嗎?', 'test_user');
  console.log('--- 最終 AI 回覆內容 ---:\n' + reply);
}

function initKnowledgeBaseSheet() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = ss.getSheetByName(CONFIG.SHEET_NAME);
  if (!sheet) {
    sheet = ss.insertSheet(CONFIG.SHEET_NAME);
  } else {
    sheet.clear();
  }

  sheet.appendRow(['分類', '標題 / 業務項目', '詳細內容與設定步驟', '狀態', '更新時間']);
  sheet.getRange('A1:E1').setBackground('#0f766e').setFontColor('#ffffff').setFontWeight('bold');

  const initialData = [
    [
      "校園網路與帳號",
      "校內無線網路 (hc-school / hc-center) 連線設定",
      `1. 請先用手機開啟熱點讓筆電/電腦連線。
2. 連線至網路中心:https://center.hc.edu.tw
3. 點選左側「Win7~11設定」,安裝第2點「win7 64位元程式 (Aruba GTC Client)」➔ 一直按下一步安裝完畢。
4. 安裝第3點「SSID自動設定下載」➔ 執行後出現問題按 y,然後按兩次 Enter 結束。
5. 連線無線網路 hc-school,跳出連線驗證框(有三格),只要在前兩格輸入您的新竹市研習護照帳密(即 tmail 帳密),第一次需輸入兩遍,即可連線成功。`,
      "啟用", new Date()
    ],
    [
      "校園網路與帳號",
      "教育部教育雲與新竹市漫遊帳號串接",
      `請記得申請教育雲帳號,並將新竹市教師研習護照/tmail 帳號與教育雲進行串接綁定。未來在校外各教育平台可直接透過「新竹市漫遊登入」,系統會自動識別竹光國中與教師身分。`,
      "啟用", new Date()
    ],
    [
      "印表機與列印",
      "全校各辦公室印表機 IP 列表 (Sharp 影印機)",
      `竹光國中各辦公室印表機 IP(網段皆為 120.104.14.?):
• 8導 1樓 (創意樓):120.104.14.215
• 8導 2樓 (創意樓):120.104.14.216
• 7導 3樓 (創意樓):120.104.14.217
• 7導 4樓 (創意樓):120.104.14.218
• 9導 3樓 (合作樓):120.104.14.228
• 3樓專任 (理性樓):120.104.14.225
• 4樓專任 (理性樓):120.104.14.227
• 3樓專任 (築夢樓):120.104.14.237`,
      "啟用", new Date()
    ],
    [
      "印表機與列印",
      "筆電/電腦安裝印表機驅動與密碼設定步驟",
      `1. 連上校網後,進入網芳:\\\\120.104.14.3 ➔ 進入「09常用好用軟體」➔「00-112年影印機」➔ 點擊執行「影印機驅動.exe」。
2. 輸入您所屬辦公室的印表機 IP(例如 8導1樓為 120.104.14.215),一路按下一步完成安裝。
3. 設定個人用戶號碼:至 Windows 設定 ➔ 裝置 ➔ 印表機與掃描器 ➔ 點選 Sharp 影印機 ➔「管理」➔「列印喜好設定」➔ 切換到上方「工作處理」標籤 ➔ 在「用戶號碼」輸入您的個人列印密碼,並將「自動工作控制預覽」打勾 ➔ 按確定存檔。`,
      "啟用", new Date()
    ],
    [
      "網路芳鄰與軟體",
      "校園網路芳鄰 (網芳) 連線位置與限制",
      `• 竹光網芳最新 IP 為:\\\\120.104.14.3
• 限制說明:網芳「僅限校內網路連線」,在家中或校外無法直接登入。
• 內含資源:教學資料互傳包、行政常用資料表單、好用軟體安裝區(防毒、印表機驅動等)。
• 連線方式:Windows 按 Win+R 輸入 \\\\120.104.14.3;Mac 於 Finder 按 Cmd+K 輸入 smb://120.104.14.3。`,
      "啟用", new Date()
    ],
    [
      "網路芳鄰與軟體",
      "公務電腦防毒軟體安裝與認證更新",
      `1. 進入網芳 \\\\120.104.14.3 ➔ 進入「09常用好用軟體」➔「01防毒掃毒」➔「112防毒軟體」。
2. 複製「PROTE_Installer_x64_zh_TW.exe」到桌面並執行安裝,完成後請「重新開機」。
3. 重開機後,請確保電腦連上校園網路 hc-school,打開防毒軟體點選「檢查更新」,放著等待伺服器認證完成。`,
      "啟用", new Date()
    ],
    [
      "校外請假VPN",
      "手機版 VPN 連線人事請假系統 (FortiClient iOS/Android)",
      `因市網將人事請假系統設於防火牆內,校外需透過 VPN:
1. 手機下載安裝「FortiClient VPN」App。
2. 新增連線設定:
   - 連線名稱:自訂(如:新竹市請假)
   - Port:10443
   - User:輸入您的 tmail 帳號
3. 點右上角儲存後返回首頁開啟 VPN,輸入 tmail 帳密即可連通並登入人事差勤請假系統。`,
      "啟用", new Date()
    ],
    [
      "校外請假VPN",
      "桌機/筆電版 VPN 連線人事請假系統 (FortiClient Windows/Mac)",
      `1. 前往網路中心下載 FortiClient VPN 並安裝。
2. 打開程式新增 VPN 連線:
   - 遠程網關 (Remote Gateway):163.19.150.9
   - 自定義端口 (Port):10443
3. 按保存後,在登入畫面輸入您的 tmail 帳號與密碼,點擊連線成功後即可開啟人事請假系統。`,
      "啟用", new Date()
    ],
    [
      "數位研習管考",
      "教育部與新竹市數位研習管考項目與完成規定",
      `全體編制內教師皆需完成以下數位學習管考研習:
1. 【教育部管考】:A1 數位學習工作坊、A2 數位學習工作坊、A3 數位素養增能研習課程(於 edu 磨課師+ 平台線上修課,竹光目標完成率 100%)。
2. 【新竹市管考】:新竹市平板初階研習、新竹市平板進階研習、iPad 基礎線上研習(7-8月)。
• 查詢路徑:登入「新竹市教師研習護照」➔ 點選「我的數位學習」即可檢視已通過時數與進度。`,
      "啟用", new Date()
    ],
    [
      "數位研習管考",
      "A3 數位素養增能研習線上報名與研習流程",
      `1. 使用 tmail 帳號密碼登入教育部「edu 磨課師+」平台。
2. 搜尋並選修「A3 數位素養增能研習課程」。
3. 線上完成課程章節並通過總測驗,成績及格即取得時數,系統將自動介接至全國教師在職進修網。`,
      "啟用", new Date()
    ],
    [
      "1Campus與校務",
      "1Campus 智慧校園系統開通與導師功能",
      `1. 首次啟用請洽【教務處註冊組】領取個人專屬「第一次通行代碼」(確保為竹光師生身分)。
2. 導師功能:可查詢導師生之個資、學期成績、獎懲與服務學習紀錄;可查詢任教班級成績;亦可向有安裝 1Campus App 的家長/學生發布班級重要推播通知。`,
      "啟用", new Date()
    ],
    [
      "資安與設備借用",
      "校園資安紅線與個人設備網路規範",
      `1. 【嚴禁自設設備】:學校絕對禁止私自裝設個人 Hub、Switch 或個人無線 WiFi 基地台,亦嚴禁以校網私開熱點分享。
2. 【資安監控】:校內網路連線由新竹市網路中心即時監控,個人設備連校網請務必升級至 Windows 11 並維持防毒軟體常駐更新。
3. 【信箱管理】:公務信箱(tmail/edu)請與個人私人信箱分開,避免公私混用。
4. 嚴重的資安違規事件將列入年終考評,請各位老師務必共同維護校園網路安全。`,
      "啟用", new Date()
    ],
    [
      "資安與設備借用",
      "資訊器材與電腦教室空間借用流程",
      `學校提供教學用平板車、筆電、投影設備與電腦教室空間借用。如需借用請至【合作樓 2 樓資訊組】登記,或透過校內行政表單系統預約借用。`,
      "啟用", new Date()
    ],
    [
      "平板與場地借用",
      "校園平板車與教室空間即時借用系統(線上登記與 LINE 查詢)",
      `1. 【線上借用系統網址】:https://tablet-checkout.pages.dev/bookings
2. 【列管平板車】:包含第九車、第十車、第十一車、第十二車、第十四車、第十五車。
3. 【列管專用教室】:1樓卓越基地、3樓電腦教室、3樓未來基地、1樓沙特教室、1樓盧梭教室、1樓黑格爾教室、4樓階梯教室等。
4. 【LINE 智慧查詢】:在 LINE 直接問「8/31 第二節有沒有人借平板車?」、「這節課誰借平板?」、「今天1樓卓越基地誰借?」、「八月31日有平板車可以借嗎?」,小幫手會自動連線即時資料庫回覆!`,
      "啟用", new Date()
    ],
    [
      "一般資訊業務",
      "Win7 64位元程式下載連結",
      `Win7 64位元程式下載連結:https://center.hc.edu.tw/wireless/win10/EAP-GTC-x64.msi ; SSID自動設定程式下載連結:https://center.hc.edu.tw/wireless/ssid_vista.exe`,
      "啟用", new Date()
    ],
    [
      "一般資訊業務",
      "SSID自動設定程式下載連結:",
      `SSID自動設定程式下載連結:https://center.hc.edu.tw/wireless/ssid_vista.exe`,
      "啟用", new Date()
    ],
    [
      "一般資訊業務",
      "Win7 64位元程式下載連結",
      `Win7 64位元程式下載連結:https://center.hc.edu.tw/wireless/win10/EAP-GTC-x64.msi`,
      "啟用", new Date()
    ],
    [
      "一般資訊業務",
      "Android手機設定方式: ",
      `Android手機設定方式: https://center.hc.edu.tw/wireless/mobile/02.php`,
      "啟用", new Date()
    ],
    [
      "一般資訊業務",
      "筆電網路安裝影片說明: htt",
      `筆電網路安裝影片說明: https://youtu.be/D4TvuiI80Yk`,
      "啟用", new Date()
    ],
    [
      "一般資訊業務",
      "以後講到要下載的程式,也要附上",
      `以後講到要下載的程式,也要附上連結說明`,
      "啟用", new Date()
    ],
    [
      "一般資訊業務",
      "給英文網址即可,不用給連結。以",
      `給英文網址即可,不用給連結。以後只要遇到要下載的,給英文網址即可。`,
      "啟用", new Date()
    ],
    [
      "一般資訊業務",
      "新生基本資料的網芳路徑為:教務",
      `新生基本資料的網芳路徑為:教務處下載區-03.註冊組-03.學生名單---含學生名條`,
      "啟用", new Date()
    ],
    [
      "115學年度暑假作業",
      "有關115學年度暑假作業抽查,",
      `有關115學年度暑假作業抽查,9/14前完成批閱與登記;9/15送件;9/17領回;9/18通知;
9/21~9/23補交;9/24確認並送出獎懲。

抽查週|學藝股長要做什麼?
• 9/14(一)以前:協助確認任課教師已完成批閱與登記。
• 9/15(二)第一節下課前:各科作業分開並依座號排序,連同抽查單正本送至二樓會議室,按班級牌置放。
• 9/17(四)第六節下課:至二樓會議室領回全班作業與抽查單影本。
• 9/18(五)放學前:學藝股長通知抽查單上的未通過學生,並告知下週補交資訊。
※新生的英語科作業,由於是單字測驗,直接由任課教師寫上分數達標的學生至優良名單。

補交週|學生怎麼做?
• 9/21(一)~9/23(三):補齊作業,先請任課教師批閱或簽名,再自行送課研組補交。
• 9/23(三)放學前補交截止;逾期未補交者,依未通過科目列入獎懲。
• 9/24(四)學藝股長帶回抽查單影本,放學前確認獎懲紀錄;如有疑問,請於放學前洽課研組。

登記表與獎懲原則
• 特殊狀況不必繳交請填「座號(原因)」,例如:2(自學);該作業不送課研組。
• 各科作業準時繳交且2科以上優良者,給予嘉獎乙支。每有1科作業不合格開立勸導單一張。`,
      "啟用", new Date()
    ],
    [
      "一般資訊業務",
      "ischool新生家長和學生代",
      `ischool新生家長和學生代碼從115學年度起,將預設為「學生學號+學生身分證後5碼」`,
      "啟用", new Date()
    ],
    [
      "一般資訊業務",
      "合作樓2樓教務處資訊處 找大乃",
      `合作樓2樓教務處資訊組 找大乃老師 或分機614`,
      "啟用", new Date()
    ],
    [
      "教室設備與大屏",
      "ViewBoard 大屏無畫面、黑屏與不開機排除SOP",
      `1. 【電源燈號判定】:
  - 亮紅燈(待機中):按一下螢幕前方實體 Power 鍵或遙控器開機(燈轉藍色即開機)。
  - 不亮燈(總電源未開):檢查大屏右下角/背面的「實體翹板開關」切至 [I],並檢查電源線。
  2. 【有開機但黑屏】:用手指觸碰螢幕任意處喚醒,或由螢幕兩側向內滑動切換至正確訊號源(HDMI 1/2 或 Type-C)。`,
      "啟用", new Date()
    ],
    [
      "教室設備與大屏",
      "ViewBoard 外接筆電有畫面但無觸控、無聲音排除",
      `1. 【有畫面無觸控(最常見)】:
  - HDMI 只傳送影像,請務必額外加插一條「USB Touch 觸控方頭線(Type-A 轉 Type-B)」連接筆電與大屏前置 Touch 孔。
  - 若用 Type-C,請確認線材為全功能影音傳輸線(非一般手機充電線)。
  2. 【無聲音或聲音從筆電發出】:
  - Windows:點右下角喇叭圖示,將播放裝置切換為「ViewBoard」。
  - Mac:控制中心 -> 聲音 -> 輸出選「ViewBoard」。
  - 檢查大屏右側或底部音量未被靜音。`,
      "啟用", new Date()
    ],
    [
      "教室設備與大屏",
      "ViewBoard 觸控斷筆、飄移與側邊工具列叫出技巧",
      `1. 【觸控斷筆與飄移排除】:
  - ViewBoard 採用紅外線光學感應,若「邊框溝槽卡粉筆灰、貼紙或磁鐵遮擋」會導致失靈。請用眼鏡布擦拭四週邊框凹槽。
  - 寫字時避免手掌或手腕大面積貼在大屏邊框上。
  2. 【叫出側邊工具列與白板】:
  - 手勢1:螢幕最左側或最右側邊緣「向內滑動」。
  - 手勢2:兩隻手指「同時長按螢幕中央」叫出圓盤選單。
  - 手勢3:底部向上滑動叫出首頁與返回鍵。`,
      "啟用", new Date()
    ],
    [
      "教室設備與大屏",
      "ViewBoard 無線投影 (Airsync / AirPlay) 與隨身碟讀取設定",
      `1. 【無線投影 (Airsync】:
  - 裝置與大屏必須連至「同一個校園 Wi-Fi 網段 (hc-school)」。
  - 大屏點開「Airsync」App,輸入畫面上顯示的 6 位數 PIN 碼進行鏡像。
  2. 【隨身碟讀不到】:
  - 請插在螢幕前置面板的「Public / Android USB」插孔。
  - 隨身碟格式建議格式化為 FAT32 或 NTFS。
  - 首頁 -> 檔案夾 (檔案管理員) -> 左側選隨身碟開啟檔案。`,
      "啟用", new Date()
    ],
    [
      "教室設備與大屏",
      "ViewBoard 大屏無畫面、黑屏與不開機排除SOP",
      `1. 【電源燈號判定】:
  - 亮紅燈(待機中):按一下螢幕前方實體 Power 鍵或遙控器開機(燈轉藍色即開機)。
  - 不亮燈(總電源未開):檢查大屏右下角/背面的「實體翹板開關」切至 [I],並檢查電源線。
 2. 【有開機但黑屏】:用手指觸碰螢幕任意處喚醒,或由螢幕兩側向內滑動切換至正確訊號源(HDMI 1/2 或 Type-C)。
 3. 【官方教學影片】:https://www.youtube.com/watch?v=eb2b9bZrzhk
 4. 【ViewBoard完整教學清單】:https://www.youtube.com/playlist?list=PLYJzE9PqG-iTwrXuKZfwNr9dpLDwnYIOd
 5. 【求助窗口】:合作樓2F資訊組 分機614。`,
      "啟用", new Date()
    ],
    [
      "教室設備與大屏",
      "ViewBoard 外接筆電有畫面但無觸控、無聲音排除",
      `1. 【有畫面無觸控(最常見)】:
  - HDMI 只傳送影像,請務必額外加插一條「USB Touch 觸控方頭線(Type-A 轉 Type-B)」連接筆電與大屏前置 Touch 孔。
  - 若用 Type-C,請確認線材為全功能影音傳輸線(非一般手機充電線)。
 2. 【無聲音或聲音從筆電發出】:
  - Windows:點右下角喇叭圖示,將播放裝置切換為「ViewBoard」。
  - Mac:控制中心 -> 聲音 -> 輸出選「ViewBoard」。
  - 檢查大屏右側或底部音量未被靜音。
 3. 【外接電腦教學影片】:https://www.youtube.com/watch?v=VILmQ0ssg5Y`,
      "啟用", new Date()
    ],
    [
      "教室設備與大屏",
      "ViewBoard 觸控斷筆、飄移與側邊工具列叫出技巧",
      `1. 【觸控斷筆與飄移排除】:
  - ViewBoard 採用紅外線光學感應,若「邊框溝槽卡粉筆灰、貼紙或磁鐵遮擋」會導致失靈。請用眼鏡布擦拭四週邊框凹槽。
  - 寫字時避免手掌或手腕大面積貼在大屏邊框上。
 2. 【叫出側邊工具列與白板】:
  - 手勢1:螢幕最左側或最右側邊緣「向內滑動」。
  - 手勢2:兩隻手指「同時長按螢幕中央」叫出圓盤選單。
  - 手勢3:底部向上滑動叫出首頁與返回鍵。
 3. 【工具列叫出示範影片】:https://www.youtube.com/watch?v=-uBQl-vDGmc
 4. 【清潔保養影片】:https://www.youtube.com/watch?v=gFa6Fj9cZ7A`,
      "啟用", new Date()
    ],
    [
      "教室設備與大屏",
      "ViewBoard 無線投影 (Airsync / AirPlay) 與隨身碟讀取設定",
      `1. 【無線投影 (Airsync/AirPlay)】:
  - 裝置與大屏必須連至「同一個校園 Wi-Fi 網段 (hc-school)」。
  - 大屏點開「Airsync」App,輸入畫面上顯示的 6 位數 PIN 碼進行鏡像。
 2. 【隨身碟讀不到】:
  - 請插在螢幕前置面板的「Public / Android USB」插孔。
  - 隨身碟格式建議格式化為 FAT32 或 NTFS。
  - 首頁 -> 檔案夾 (檔案管理員) -> 左側選隨身碟開啟檔案。
 3. 【隨身碟讀檔教學影片】:https://www.youtube.com/watch?v=lJXQZ35kpv8`,
      "啟用", new Date()
    ],
    [
      "1Campus與竹市校園智慧通",
      "新竹市親師生平台與竹市校園智慧通家長代碼親子綁定SOP",
      `1. 【平台網址與App下載】:
  - 網頁版:https://pts.hc.edu.tw/
  - 手機下載:App Store / Google Play 搜尋「竹市校園智慧通」。
 2. 【操作步驟】:
  - 步驟1:打開 App 選擇以「手機號碼」註冊並接收簡訊驗證碼登入。
  - 步驟2:登入後點選「身分管理」->「加入身分」。
  - 步驟3:輸入學校導師發放的「專屬家長代碼」即可完成綁定!求助單位:教務處註冊組
 3. 【功能】:綁定後可即時查閱學生出缺席、學期成績、聯絡簿、學雜費繳費與校園公告。
 4. 【官方操作教學影片】:https://www.youtube.com/watch?v=UpIuKzQZ_M4    ;    https://www.youtube.com/watch?v=FhLDhQX19JA`,
      "啟用", new Date()
    ],
    [
      "1Campus與竹市校園智慧通",
      "竹市校園智慧通家長代碼遺失、多子女綁定與客服求助",
      `1. 【家長代碼遺失】:代碼於開學由班級導師發放通知單。若遺失,請向班級導師或教務處資訊組(分機 614)查詢補發。
 2. 【家有多位子女】:不需要申請多個帳號!同一個家長帳號,只需進入「身分管理」再次點選「加入身分」,輸入另一位子女的家長代碼即可同時管理。
 3. 【官方教學影片】:https://www.youtube.com/watch?v=UpIuKzQZ_M4       ;     https://www.youtube.com/watch?v=FhLDhQX19JA
 4. 【官方線上LINE客服】:https://line.me/R/ti/p/@190dngoq (LINE ID: @190dngoq)。
竹市校園智慧通家長代碼遺失 求助單位:教務處註冊組`,
      "啟用", new Date()
    ],
    [
      "1Campus與竹市校園智慧通",
      "家長登入竹市校園智慧通APP的方式",
      `關於家長登入「1Campus(竹市校園智慧通)」App 的方式,整理說明如下:

【1Campus 智慧校園系統登入指南】

📱 第一步:下載 App
請家長先至手機的 App Store 或 Google Play 搜尋並下載「1Campus」或「卓視校園智慧通」。

🔑 第二步:取得通行代碼
1. 首次啟用時,請家長洽詢【教務處註冊組】領取個人專屬的「第一次通行代碼」,以確保家長與學生的身分綁定。
2. 針對 115 學年度起的新生,家長與學生的代碼預設為:「學生學號」+「學生身分證字號後 5 碼」。

🏫 第三步:功能與應用
登入成功後,家長即可接收學校或導師發布的重要推播通知,並隨時查詢孩子的學期成績、獎懲紀錄與服務學習紀錄。

💡 小提醒
1. 若家長遺失代碼或無法登入,請引導家長洽詢【教務處註冊組】重新查詢。
2. 【官方教學影片】:https://www.youtube.com/watch?v=UpIuKzQZ_M4  ;    新竹市親師生平台暨校園智慧通家長使用「掃QR Code」做親子綁定- YouTube
3. 【官方線上LINE客服】:https://line.me/R/ti/p/@190dngoq (LINE ID: @190dngoq)。

若還有其他操作上的問題,歡迎隨時找我!竹市校園智慧通家長代碼遺失`,
      "", new Date()
    ],
    [
      "1Campus與校務",
      "竹光國中教務處各組業務執掌與諮詢窗口(註冊/課研/資訊/教學組)",
      `1. 【註冊組】:負責 1Campus「竹市校園智慧通」、家長代碼發放與親子綁定、學生學籍與成績管理、補考業務。
2. 【課研組】:負責作業抽查/作業檢查、課發會、班級共讀書箱、書展週活動。
3. 【資訊組(大乃老師 分機614)】:負責校園網路 (hc-school)、網芳、印表機、請假VPN、數位研習管考、ViewBoard大屏觸屏故障維護、平板車與專用教室線上借用系統、設備線上報修 (#報修)。
4. 【教學組】:負責定期評量(段考)、模擬考、課務排課、第八節輔導課與自習班業務。
5. 【行事曆活動分流】:行事曆活動依標註由該組負責:(註)=註冊組、(課)=課研組、(訊)=資訊組、(教)=教學組。`,
      "啟用", new Date()
    ],
    [
      "教室設備與大屏",
      "ViewBoard 大屏無畫面、黑屏與不開機排除SOP",
      `1. 【電源燈號判定】:
   - 亮紅燈(待機中):按一下螢幕前方實體 Power 鍵或遙控器開機(燈轉藍色即開機)。
   - 不亮燈(總電源未開):檢查大屏右下角/背面的「實體翹板開關」切至 [I],並檢查電源線。
2. 【有開機但黑屏】:用手指觸碰螢幕任意處喚醒,或由螢幕兩側向內滑動切換至正確訊號源(HDMI 1/2 或 Type-C)。
3. 【官方教學影片】:https://www.youtube.com/watch?v=eb2b9bZrzhk
4. 【ViewBoard完整教學清單】:https://www.youtube.com/playlist?list=PLYJzE9PqG-iTwrXuKZfwNr9dpLDwnYIOd
5. 【求助窗口】:合作樓2F資訊組 分機614。`,
      "啟用", new Date()
    ],
    [
      "教室設備與大屏",
      "ViewBoard 外接筆電有畫面但無觸控、無聲音排除",
      `1. 【有畫面無觸控(最常見)】:
   - HDMI 只傳送影像,請務必額外加插一條「USB Touch 觸控方頭線(Type-A 轉 Type-B)」連接筆電與大屏前置 Touch 孔。
   - 若用 Type-C,請確認線材為全功能影音傳輸線(非一般手機充電線)。
2. 【無聲音或聲音從筆電發出】:
   - Windows:點右下角喇叭圖示,將播放裝置切換為「ViewBoard」。
   - Mac:控制中心 -> 聲音 -> 輸出選「ViewBoard」。
   - 檢查大屏右側或底部音量未被靜音。
3. 【外接電腦教學影片】:https://www.youtube.com/watch?v=VILmQ0ssg5Y`,
      "啟用", new Date()
    ],
    [
      "教室設備與大屏",
      "ViewBoard 觸控斷筆、飄移與側邊工具列叫出技巧",
      `1. 【觸控斷筆與飄移排除】:
   - ViewBoard 採用紅外線光學感應,若「邊框溝槽卡粉筆灰、貼紙或磁鐵遮擋」會導致失靈。請用眼鏡布擦拭四週邊框凹槽。
   - 寫字時避免手掌或手腕大面積貼在大屏邊框上。
2. 【叫出側邊工具列與白板】:
   - 手勢1:螢幕最左側或最右側邊緣「向內滑動」。
   - 手勢2:兩隻手指「同時長按螢幕中央」叫出圓盤選單。
   - 手勢3:底部向上滑動叫出首頁與返回鍵。
3. 【工具列叫出示範影片】:https://www.youtube.com/watch?v=-uBQl-vDGmc
4. 【清潔保養影片】:https://www.youtube.com/watch?v=gFa6Fj9cZ7A`,
      "啟用", new Date()
    ],
    [
      "教室設備與大屏",
      "ViewBoard 無線投影 (vCast / AirPlay) 與隨身碟讀取設定",
      `1. 【無線投影 (vCast/AirPlay)】:
   - 裝置與大屏必須連至「同一個校園 Wi-Fi 網段 (hc-school)」。
   - 大屏點開「vCast」App,輸入畫面上顯示的 6 位數 PIN 碼進行鏡像。
2. 【隨身碟讀不到】:
   - 請插在螢幕前置面板的「Public / Android USB」插孔。
   - 隨身碟格式建議格式化為 FAT32 或 NTFS。
   - 首頁 -> 檔案夾 (檔案管理員) -> 左側選隨身碟開啟檔案。
3. 【隨身碟讀檔教學影片】:https://www.youtube.com/watch?v=lJXQZ35kpv8`,
      "啟用", new Date()
    ],
    [
      "1Campus與校務",
      "新竹市親師生平台與竹市校園智慧通家長代碼親子綁定SOP",
      `1. 【平台網址與App下載】:
   - 網頁版:https://pts.hc.edu.tw/
   - 手機下載:App Store / Google Play 搜尋「竹市校園智慧通」。
2. 【操作步驟】:
   - 步驟1:打開 App 選擇以「手機號碼」註冊並接收簡訊驗證碼登入。
   - 步驟2:登入後點選「身分管理」->「加入身分」。
   - 步驟3:輸入學校導師發放的「專屬家長代碼」即可完成綁定!
3. 【功能】:綁定後可即時查閱學生出缺席、學期成績、聯絡簿、學雜費繳費與校園公告。
4. 【業務負責】:由教務處【註冊組】主辦。
5. 【官方操作教學影片】:https://www.youtube.com/watch?v=UpIuKzQZ_M4`,
      "啟用", new Date()
    ],
    [
      "1Campus與校務",
      "竹市校園智慧通家長代碼遺失、多子女綁定與客服求助",
      `1. 【家長代碼遺失】:代碼於開學由班級導師發放通知單。若遺失,請向教務處【註冊組】或資訊組(分機 614)查詢補發。
2. 【家有多位子女】:不需要申請多個帳號!同一個家長帳號,只需進入「身分管理」再次點選「加入身分」,輸入另一位子女的家長代碼即可同時管理。
3. 【官方教學影片】:https://www.youtube.com/watch?v=UpIuKzQZ_M4
4. 【官方線上LINE客服】:https://line.me/R/ti/p/@190dngoq (LINE ID: @190dngoq)。`,
      "啟用", new Date()
    ],
    [
      "1Campus與校務",
      "竹光國中教務處行事曆與段考重要時程查詢(Google線上日曆)",
      `1. 【教務處行事曆線上日曆】:https://calendar.google.com/calendar/embed?src=mail.zgjh.hc.edu.tw_ivesgumrre3jooutko74ts6ilg%40group.calendar.google.com&ctz=Asia%2FTaipei
2. 【包含時程與分流】:定期評量/段考(教學組)、課發會/作業抽查(課研組)、科展/資安/大屏(資訊組)、補考/智慧通(註冊組)。
3. 【LINE 智慧查詢】:在 LINE 直接詢問「這學期段考什麼時候?」、「下週教務處有什麼活動?」、「科展報名時間」,小幫手會自動連線 Google 行事曆即時回答!`,
      "啟用", new Date()
    ]
  ];

  sheet.getRange(2, 1, initialData.length, 5).setValues(initialData);
  sheet.autoResizeColumns(1, 5);

  // 初始化報修工作表
  let repairSheet = ss.getSheetByName(CONFIG.REPAIR_SHEET_NAME);
  if (!repairSheet) {
    repairSheet = ss.insertSheet(CONFIG.REPAIR_SHEET_NAME);
    repairSheet.appendRow(['報修單號', '登記時間', '報修者ID', '報修地點與故障內容', '處理狀態', '處理備註', '完成時間']);
    repairSheet.getRange('A1:G1').setBackground('#b91c1c').setFontColor('#ffffff').setFontWeight('bold');
  }

  // 初始化未解問題收集表
  let unansweredSheet = ss.getSheetByName(CONFIG.UNANSWERED_SHEET_NAME);
  if (!unansweredSheet) {
    unansweredSheet = ss.insertSheet(CONFIG.UNANSWERED_SHEET_NAME);
    unansweredSheet.appendRow(['記錄時間', '提問者ID', '未命中之提問問題', 'AI當次回覆', '處理狀態', '大乃老師補充解答']);
    unansweredSheet.getRange('A1:F1').setBackground('#c2410c').setFontColor('#ffffff').setFontWeight('bold');
  }

  console.log('竹光資訊組知識庫初始化完成,共匯入 ' + initialData.length + ' 筆資料!');
}
index.html (後台視覺化知識庫控制面板)
<!DOCTYPE html>
<html lang="zh-TW">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>校園資訊組 智慧小幫手控制台</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
  <style>
    body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans TC", sans-serif; }
  </style>
</head>
<body class="bg-slate-100 text-slate-800 min-h-screen">

  <!-- 頂部導航 -->
  <header class="bg-teal-900 text-white shadow-md">
    <div class="max-w-6xl mx-auto px-4 py-4 flex items-center justify-between">
      <div class="flex items-center space-x-3">
        <div class="w-10 h-10 rounded-xl bg-teal-500 flex items-center justify-center text-xl font-bold">
          <i class="fa-solid fa-laptop-code"></i>
        </div>
        <div>
          <h1 class="text-lg font-bold">校園資訊組 AI 智慧小幫手</h1>
          <p class="text-xs text-teal-200">LINE Bot 智慧知識庫・視覺化管理後台</p>
        </div>
      </div>
      <div class="flex items-center space-x-2">
        <span class="px-3 py-1 bg-teal-800 border border-teal-600 rounded-full text-xs text-teal-200">
          <i class="fa-solid fa-circle text-emerald-400 text-[10px] mr-1"></i>Gemini RAG 運行中
        </span>
      </div>
    </div>
  </header>

  <!-- 主內容區 -->
  <main class="max-w-6xl mx-auto px-4 py-8 space-y-6">

    <!-- 快捷分類標籤與搜尋 -->
    <div class="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm space-y-4">
      <div class="flex flex-col sm:flex-row items-center justify-between gap-3">
        <div class="relative w-full sm:w-80">
          <i class="fa-solid fa-magnifying-glass absolute left-3 top-3 text-slate-400 text-sm"></i>
          <input type="text" id="searchInput" placeholder="搜尋業務、網路、帳號、研習、平板、排障..." 
            class="w-full pl-9 pr-4 py-2 text-sm border border-slate-300 rounded-xl focus:ring-2 focus:ring-teal-500 focus:outline-none"
            oninput="filterCards()">
        </div>
        <div class="flex flex-wrap gap-1.5 text-xs">
          <button onclick="filterCategory('ALL')" class="cat-btn px-3 py-1.5 rounded-lg bg-teal-700 text-white font-medium">全部</button>
          <button onclick="filterCategory('校園網路與帳號')" class="cat-btn px-3 py-1.5 rounded-lg bg-slate-100 text-slate-600 hover:bg-slate-200">網路帳號</button>
          <button onclick="filterCategory('數位教學與軟體')" class="cat-btn px-3 py-1.5 rounded-lg bg-slate-100 text-slate-600 hover:bg-slate-200">教學軟體</button>
          <button onclick="filterCategory('數位研習管考')" class="cat-btn px-3 py-1.5 rounded-lg bg-slate-100 text-slate-600 hover:bg-slate-200">數位研習</button>
          <button onclick="filterCategory('校務系統與行政')" class="cat-btn px-3 py-1.5 rounded-lg bg-slate-100 text-slate-600 hover:bg-slate-200">校務系統</button>
          <button onclick="filterCategory('平板與設備借用')" class="cat-btn px-3 py-1.5 rounded-lg bg-slate-100 text-slate-600 hover:bg-slate-200">平板借用</button>
          <button onclick="filterCategory('校園資安與個資')" class="cat-btn px-3 py-1.5 rounded-lg bg-slate-100 text-slate-600 hover:bg-slate-200">資安規範</button>
          <button onclick="filterCategory('教室設備排障')" class="cat-btn px-3 py-1.5 rounded-lg bg-slate-100 text-slate-600 hover:bg-slate-200">設備排障</button>
          <button onclick="filterCategory('設備報修與耗材')" class="cat-btn px-3 py-1.5 rounded-lg bg-slate-100 text-slate-600 hover:bg-slate-200">報修耗材</button>
        </div>
      </div>
    </div>

    <!-- 知識庫卡片清單 -->
    <div id="cardsGrid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
      <div class="col-span-full text-center py-12 text-slate-400 text-sm">
        <i class="fa-solid fa-spinner animate-spin mr-2"></i>載入校園資訊知識庫中...
      </div>
    </div>

  </main>

  <script>
    let rawKnowledgeData = [];
    let currentCategory = 'ALL';

    window.onload = function() {
      google.script.run.withSuccessHandler(renderData).getKnowledgeBaseData();
    };

    function renderData(data) {
      rawKnowledgeData = data || [];
      filterCards();
    }

    function filterCategory(cat) {
      currentCategory = cat;
      document.querySelectorAll('.cat-btn').forEach(b => {
        b.className = 'cat-btn px-3 py-1.5 rounded-lg bg-slate-100 text-slate-600 hover:bg-slate-200';
      });
      event.target.className = 'cat-btn px-3 py-1.5 rounded-lg bg-teal-700 text-white font-medium';
      filterCards();
    }

    function filterCards() {
      const kw = document.getElementById('searchInput').value.toLowerCase();
      const grid = document.getElementById('cardsGrid');
      grid.innerHTML = '';

      const filtered = rawKnowledgeData.filter(item => {
        const matchCat = (currentCategory === 'ALL' || item.category === currentCategory);
        const matchKw = (!kw || item.title.toLowerCase().includes(kw) || item.content.toLowerCase().includes(kw) || item.category.toLowerCase().includes(kw));
        return matchCat && matchKw;
      });

      if (filtered.length === 0) {
        grid.innerHTML = '<div class="col-span-full text-center py-12 text-slate-400 text-sm">查無符合條件的項目</div>';
        return;
      }

      filtered.forEach(item => {
        const card = document.createElement('div');
        card.className = 'bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex flex-col justify-between space-y-3 hover:shadow-md transition-shadow';
        card.innerHTML = `
          <div class="space-y-2">
            <div class="flex items-center justify-between">
              <span class="text-[11px] font-bold px-2.5 py-0.5 rounded-full bg-teal-50 text-teal-700 border border-teal-200">${item.category}</span>
              <span class="text-xs text-emerald-600 font-semibold"><i class="fa-solid fa-check mr-1"></i>啟用中</span>
            </div>
            <h3 class="font-bold text-slate-900 text-sm leading-snug">${item.title}</h3>
            <p class="text-xs text-slate-600 whitespace-pre-line leading-relaxed max-h-48 overflow-y-auto">${item.content}</p>
          </div>
          <div class="pt-2 border-t border-slate-100 flex items-center justify-between text-[11px] text-slate-400">
            <span>項目 ID: #${item.id}</span>
            <span class="text-teal-700 font-medium">校園資訊組</span>
          </div>
        `;
        grid.appendChild(card);
      });
    }
  </script>
</body>
</html>

常見問題與新手排錯(FAQ)

遇到問題別擔心,99% 的情況都是以下小細節:

平板借用查詢是怎麼知道「現在是第幾節課」的?

程式碼內建台灣時間 (UTC+8) 的標準學校作息節次表(早修 07:30、第一節 08:25...等),當收到提問時會自動將系統時間與當前節次換算,並精確比對借用資料庫。

LINE 點擊 Webhook Verify 出現錯誤?

請回到 Apps Script 檢查部署設定中的「誰可以存取 (Who has access)」是否選成「僅限自己」。務必切換為「所有人 (Anyone)」並重新發布新版本,LINE 伺服器才能連線成功!

在 LINE 傳訊息,機器人沒有回應?

請檢查 LINE Developers 後台的 Messaging API 頁籤中:(1) Use webhook 是否已打開 (ON);(2) Auto-reply messages 是否已設為關閉;(3) 檢查 Code.gs 第 8、9 行金鑰是否填妥並已按儲存部署。

可以把小幫手拉進學校全體教職員大群組嗎?

完全可以! 程式碼已內建群組防洗版過濾機制:在群組中,只有老師主動 @小幫手 或提及「資訊組長/小幫手」時它才會發言,平常絕不干擾群組對話。