0. 作品介紹
封面
2. 資料表規則
請照下面規則建立分頁與欄位。
第1分頁: 借用記錄總表
A 欄: 借用日期
B 欄: 類別
C 欄: 品項/場地名稱
D 欄: 借用人
E 欄: 借用節次
F 欄: 借用狀態
G 欄: 登記時間
H 欄: 備註說明
第2分頁: 設備清單
A 欄: 設備名稱
B 欄: 總數量
C 欄: 存放地點
D 欄: 配件說明
E 欄: 借用限制
第3分頁: 場地清單
A 欄: 場地名稱
B 欄: 容納人數
C 欄: 所在位置
D 欄: 場地設備
E 欄: 負責單位
第4分頁: 借用規章與常見問答
A 欄: 問題或規範主題
B 欄: 詳細內容說明
C 欄: 主管分機
第5分頁: 查詢與操作紀錄
A 欄: 紀錄時間
B 欄: 使用者ID
C 欄: 查詢內容
D 欄: 回覆摘要
📄 解析結果(自動表格)
分頁:借用記錄總表
| A 欄 | B 欄 | C 欄 | D 欄 | E 欄 | F 欄 | G 欄 | H 欄 |
|---|---|---|---|---|---|---|---|
| 借用日期 | 類別 | 品項/場地名稱 | 借用人 | 借用節次 | 借用狀態 | 登記時間 | 備註說明 |
分頁:設備清單
| A 欄 | B 欄 | C 欄 | D 欄 | E 欄 |
|---|---|---|---|---|
| 設備名稱 | 總數量 | 存放地點 | 配件說明 | 借用限制 |
分頁:場地清單
| A 欄 | B 欄 | C 欄 | D 欄 | E 欄 |
|---|---|---|---|---|
| 場地名稱 | 容納人數 | 所在位置 | 場地設備 | 負責單位 |
分頁:借用規章與常見問答
| A 欄 | B 欄 | C 欄 |
|---|---|---|
| 問題或規範主題 | 詳細內容說明 | 主管分機 |
分頁:查詢與操作紀錄
| A 欄 | B 欄 | C 欄 | D 欄 |
|---|---|---|---|
| 紀錄時間 | 使用者ID | 查詢內容 | 回覆摘要 |
3. 開啟 Apps Script
從試算表開 Apps Script,才能直接用 SpreadsheetApp 讀寫資料。
- 試算表上方選單:擴充功能 → Apps Script
- 刪掉預設的程式碼(或全部選取覆蓋)
-
保留/建立檔案:
- Code.gs
- index.html
4. 貼上後端(Code.gs)
把後端程式貼到 Apps Script 的 Code.gs。
/**
* ============================================================================
* 🏫 全國校園資訊設備與場地借用小幫手 LINE Bot・通用開源範本 (GAS + Gemini RAG)
* ============================================================================
* 專為全台各級學校(國小、國中、高中職、大專院校)資訊組、設備組、總務處與全校教職員設計!
*
* 核心功能:
* 1. 🔄 每日自動同步校園借用 API:定時排程定時抓取學校借用系統紀錄,匯入 Google 試算表。
* 2. 💻 資訊設備借用查詢:即時掌握平板車 (iPad/Chromebook)、筆電、觸控筆、實物投影機等借用現況。
* 3. 🏛️ 校園場地借用查詢:電腦教室、會議室、創客教室、視聽教室、活動中心等時段排程一目了然。
* 4. 📊 個人學期借用次數與品項統計:老師輸入姓名即可查詢本學期借用總次數、各設備品項借用分佈與近期紀錄。
* 5. 🤖 Gemini 2.5 自然語言 RAG:支援自然對話(例如「今天下午第三節有人借電腦教室一嗎?」)與精準指令雙軌並行。
* 6. 📱 內建 WebApp 借用資訊看板:提供校內儀表板即時展示今日借用動態。
* ============================================================================
*/
const CONFIG = {
// --------------------------------------------------------------------------
// 1. 必填金鑰 (請至 Google AI Studio 與 LINE Developers 後台取得)
// --------------------------------------------------------------------------
GEMINI_API_KEY: '填入您的_GEMINI_API_KEY',
LINE_ACCESS_TOKEN: '填入您的_LINE_CHANNEL_ACCESS_TOKEN',
// --------------------------------------------------------------------------
// 2. 學校基本設定與學期起始日
// --------------------------------------------------------------------------
SCHOOL_NAME: '竹光國民中學', // 學校名稱(例:竹光國中、光明國中)
UNIT_NAME: '教務處資訊組', // 主管單位(資訊組 / 設備組 / 總務處)
BOT_NAME: '校園借用小幫手', // 機器人名稱
ADMIN_CONTACT: '資訊組分機 215 / 設備組分機 214', // 管理單位聯絡電話或分機
SEMESTER_START_DATE: '2026-08-30', // 本學期起算日 (YYYY-MM-DD),用於計算個人本學期借用統計
// --------------------------------------------------------------------------
// 3. 校園借用系統 API 串接設定
// --------------------------------------------------------------------------
// 學校線上借用系統 API 網址 (GET 請求,系統支援 ?date=YYYY-MM-DD 參數)
// 若學校尚未開放 API,可留空,系統將自動以 Google 試算表作為主要資料庫並提供模擬展示資料
BOOKING_API_URL: '', // 例: https://your-school.edu.tw/api/bookings
BOOKING_WEB_URL: '', // 學校官方借用系統登入網址 (供 LINE 提供超連結)
// --------------------------------------------------------------------------
// 4. 工作表名稱設定
// --------------------------------------------------------------------------
SHEET_BOOKINGS: '借用記錄總表', // 歷史與今日借用記錄總表
SHEET_EQUIPMENT: '設備清單', // 設備類別、編號與數量設定
SHEET_VENUES: '場地清單', // 場地名稱、容納人數與位置
SHEET_FAQ: '借用規章與常見問答', // 借用辦法、領取時間、賠償規範等知識庫
SHEET_LOGS: '查詢與操作紀錄', // 查詢提問與日誌記錄表
ADMIN_PASSWORD: '' // 手動同步密碼(留空即允許群組管理員直接觸發)
};
// ============================================================================
// 🤖 動態組裝借用管理員 AI 人設 Prompt (System Instruction)
// ============================================================================
function getSystemInstruction() {
return `你是「${CONFIG.SCHOOL_NAME} ${CONFIG.UNIT_NAME}」的 AI 借用管理助理兼諮詢小幫手「${CONFIG.BOT_NAME}」。
你的職責:
1. 為全校教職員提供親切、準確、即時的「資訊設備」與「場地空間」借用狀態查詢。
2. 協助老師查詢「個人本學期借用紀錄與次數統計」,幫助老師掌握教學資源使用狀況。
3. 解答校內設備借用流程、歸還規範、教室鑰匙借還時間及注意事項。
回覆規範:
• 語言風格:親切有禮、條理分明、精準清晰(繁體中文/台灣習慣用詞)。
• 資訊呈現:善用條列點與 emoji(如 💻 設備、🏛️ 場地、📊 統計、🕒 時段、⚠️ 注意事項)。
• 查詢比對:根據提供的最新即時借用資料與知識庫回答。若時段已有預約,務必提醒借用人與節次;若無人預約,告知該時段目前尚有空檔。
• 節次習慣:台灣校園節次通常為:第1節(08:20~09:05)、第2節(09:15~10:00)、第3節(10:15~11:00)、第4節(11:10~11:55)、第5節(13:10~13:55)、第6節(14:05~14:50)、第7節(15:05~15:50)、第8節/課後輔導(16:00~16:45)。
• 當老師問到無法確認的特殊需求(如跨日外借、大批借用、設備故障報修),請引導洽詢「${CONFIG.ADMIN_CONTACT}」。`;
}
// ============================================================================
// 🌐 Web 儀表板與狀態檢查 (doGet)
// ============================================================================
function doGet(e) {
const action = (e && e.parameter) ? e.parameter.action : '';
// 提供外部或 Cron 呼叫同步 API
if (action === 'sync') {
const result = dailySyncBookingApi();
return ContentService.createTextOutput(JSON.stringify(result)).setMimeType(ContentService.MimeType.JSON);
}
// 預設渲染儀表板 HTML(優先載入 index.html,相容 gas_index)
try {
let template;
try {
template = HtmlService.createTemplateFromFile('index');
} catch(err1) {
template = HtmlService.createTemplateFromFile('gas_index');
}
template.schoolName = CONFIG.SCHOOL_NAME;
template.unitName = CONFIG.UNIT_NAME;
template.botName = CONFIG.BOT_NAME;
template.todayStr = formatDateStr(getTwNow());
return template.evaluate()
.setTitle(`${CONFIG.SCHOOL_NAME} - ${CONFIG.BOT_NAME} 資訊看板`)
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
} catch (err) {
return ContentService.createTextOutput(`【${CONFIG.SCHOOL_NAME} ${CONFIG.BOT_NAME} 系統運行中】\n目前時間:${new Date().toLocaleString('zh-TW', {timeZone: 'Asia/Taipei'})}\n狀態:正常連線`);
}
}
// ============================================================================
// 📩 LINE Webhook 處理核心 (doPost)
// ============================================================================
function doPost(e) {
try {
if (!e || !e.postData || !e.postData.contents) {
return ContentService.createTextOutput('No post data');
}
const data = JSON.parse(e.postData.contents);
const events = data.events;
if (!events || events.length === 0) {
return ContentService.createTextOutput('No events');
}
for (let i = 0; i < events.length; i++) {
const event = events[i];
if (event.type !== 'message' || event.message.type !== 'text') {
continue;
}
const replyToken = event.replyToken;
const userMessage = event.message.text.trim();
const userId = event.source.userId || 'anonymous';
const sourceType = event.source.type; // user, group, room
const chatId = (sourceType === 'user') ? userId : (event.source.groupId || event.source.roomId);
// 群組防打擾過濾:在群組中必須提到機器人名稱或以 # 開頭
if (sourceType !== 'user') {
const botTriggerRegex = new RegExp(`(@${escapeRegExp(CONFIG.BOT_NAME)}|#|借用|查詢)`, 'i');
if (!botTriggerRegex.test(userMessage)) {
continue;
}
}
// 清理群組標記文字
const cleanMessage = userMessage.replace(new RegExp(`@${escapeRegExp(CONFIG.BOT_NAME)}\\s*`, 'g'), '').trim();
if (!cleanMessage) continue;
// 觸發 LINE 原生「輸入中」動畫 (Chat Loading Indicator)
if (chatId) {
showLineLoading(chatId);
}
// 1. 快速指令路由:手動觸發 API 同步
if (/^(#同步|#更新借用|#sync)$/i.test(cleanMessage)) {
const syncRes = dailySyncBookingApi();
sendLineReply(replyToken, `🔄 【借用資料同步完成】\n• 成功更新筆數:${syncRes.count} 筆\n• 抓取來源:${syncRes.source}\n• 更新時間:${formatDateTimeStr(getTwNow())}\n\n您現在可輸入「#查設備」或「#查場地」查看最新預約狀態!`);
continue;
}
// 2. 快速指令路由:說明選單
if (/^(#說明|#功能|#選單|help|\?|說明)$/i.test(cleanMessage)) {
const helpText = `📋 【${CONFIG.BOT_NAME} 快捷查詢導覽】\n\n` +
`🔹 資訊設備查詢:\n` +
`• 輸入「#查設備」或「今天有什麼設備被借了?」\n` +
`• 輸入「#設備 平板車」或「明天有人借 iPad 車嗎?」\n\n` +
`🔹 校園場地查詢:\n` +
`• 輸入「#查場地」或「今天電腦教室借用狀況」\n` +
`• 輸入「#場地 會議室」或「明天活動中心第3節有空嗎?」\n\n` +
`🔹 個人學期借用統計:\n` +
`• 輸入「#查個人 [姓名]」或「#我的借用 [姓名]」\n` +
` (例:#查個人 陳乃誠)\n` +
` 系統將為您統計本學期借用總次數與品項分佈!\n\n` +
`🔹 借用系統更新:\n` +
`• 輸入「#同步」立即強制比對校內借用系統。\n\n` +
`💬 您也可以直接打字提問,AI 助理 24 小時隨時為您解答!`;
sendLineReply(replyToken, helpText);
continue;
}
// 3. 快速指令路由:查詢資訊設備
if (/^(#查設備|#設備)(\s*(.*))?$/i.test(cleanMessage)) {
const match = cleanMessage.match(/^(#查設備|#設備)(\s*(.*))?$/i);
const subQuery = match && match[3] ? match[3].trim() : '';
const replyText = handleEquipmentQuery(subQuery);
sendLineReply(replyToken, replyText);
logConversation(cleanMessage, replyText, userId);
continue;
}
// 4. 快速指令路由:查詢校園場地
if (/^(#查場地|#場地)(\s*(.*))?$/i.test(cleanMessage)) {
const match = cleanMessage.match(/^(#查場地|#場地)(\s*(.*))?$/i);
const subQuery = match && match[3] ? match[3].trim() : '';
const replyText = handleVenueQuery(subQuery);
sendLineReply(replyToken, replyText);
logConversation(cleanMessage, replyText, userId);
continue;
}
// 5. 快速指令路由:查詢個人學期借用統計
if (/^(#查個人|#我的借用|#個人借用)(\s*(.*))?$/i.test(cleanMessage)) {
const match = cleanMessage.match(/^(#查個人|#我的借用|#個人借用)(\s*(.*))?$/i);
const teacherName = match && match[3] ? match[3].trim() : '';
if (!teacherName) {
sendLineReply(replyToken, `請在指令後方加上您的姓名,例如:\n「#查個人 陳乃誠」或「#我的借用 王小明」`);
continue;
}
const replyText = handleTeacherStatsQuery(teacherName);
sendLineReply(replyToken, replyText);
logConversation(cleanMessage, replyText, userId);
continue;
}
// 6. 自然語言提問:走 RAG + Gemini AI 智慧理解
const aiResponse = generateAiResponse(cleanMessage, userId);
sendLineReply(replyToken, aiResponse);
logConversation(cleanMessage, aiResponse, userId);
}
return ContentService.createTextOutput('OK');
} catch (err) {
console.error('doPost Error:', err);
return ContentService.createTextOutput('Error: ' + err.toString());
}
}
// ============================================================================
// 🔄 每日定時自動同步 API 核心 (Time-driven Trigger Target)
// ============================================================================
/**
* 每日自動執行:抓取學校借用系統 API 並更新至 Google 試算表。
* 建議設定定時觸發器:每天早上 06:30 或 07:00 執行一次。
*/
function dailySyncBookingApi() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
let sheet = ss.getSheetByName(CONFIG.SHEET_BOOKINGS);
if (!sheet) {
initBookingSystemSheet();
sheet = ss.getSheetByName(CONFIG.SHEET_BOOKINGS);
}
let fetchedList = [];
let sourceName = '校園借用系統 API';
// 1. 若有設定 API URL,發送 HTTP GET 請求
if (CONFIG.BOOKING_API_URL && CONFIG.BOOKING_API_URL.trim() !== '') {
try {
const todayStr = formatDateStr(getTwNow());
const urlWithParams = CONFIG.BOOKING_API_URL.includes('?')
? `${CONFIG.BOOKING_API_URL}&date=${todayStr}`
: `${CONFIG.BOOKING_API_URL}?date=${todayStr}`;
const response = UrlFetchApp.fetch(urlWithParams, {
muteHttpExceptions: true,
headers: { 'Accept': 'application/json' }
});
if (response.getResponseCode() === 200) {
const json = JSON.parse(response.getContentText());
fetchedList = normalizeApiBookingData(json);
} else {
console.warn('API returned non-200 status:', response.getResponseCode());
}
} catch (e) {
console.error('dailySyncBookingApi fetch error:', e);
}
}
// 2. 若未設定 API 或 API 無回傳,檢查是否有現有資料;若試算表空白則載入示範種子資料
if (fetchedList.length === 0) {
const existingRows = sheet.getLastRow();
if (existingRows <= 1) {
fetchedList = getSampleBookingSeedData();
sourceName = '內建標準測試資料(請於 CONFIG 填入學校 API 網址)';
} else {
sourceName = '本地試算表資料庫 (API 無新異動)';
return { success: true, count: existingRows - 1, source: sourceName };
}
}
// 3. 匯入試算表(依據 複合鍵 進行防重複比對)
const updatedCount = mergeBookingsIntoSheet(sheet, fetchedList);
return {
success: true,
count: updatedCount,
source: sourceName,
time: formatDateTimeStr(getTwNow())
};
}
/**
* 彈性適配各種學校借用系統的 JSON 格式
*/
function normalizeApiBookingData(json) {
const result = [];
let rawItems = [];
if (Array.isArray(json)) {
rawItems = json;
} else if (json && Array.isArray(json.bookings)) {
rawItems = json.bookings;
} else if (json && Array.isArray(json.data)) {
rawItems = json.data;
} else if (json && (json.tablets || json.venues || json.equipment)) {
rawItems = [
...(json.tablets || []).map(x => ({ ...x, category: '資訊設備' })),
...(json.equipment || []).map(x => ({ ...x, category: '資訊設備' })),
...(json.venues || []).map(x => ({ ...x, category: '校園場地' }))
];
}
rawItems.forEach(item => {
const date = item.date || item.day || item.booking_date || formatDateStr(getTwNow());
const category = item.category || (isVenueName(item.item || item.name || '') ? '校園場地' : '資訊設備');
const itemName = item.item || item.name || item.title || item.equipment_name || item.venue_name || '未註明項目';
const teacher = item.teacher || item.user || item.applicant || item.borrower || item.name || '未具名教席';
let periods = item.periods || item.period || item.time || item.section || '全日';
if (Array.isArray(periods)) {
periods = periods.join('、');
}
const regTime = item.reg_time || item.created_at || formatDateTimeStr(getTwNow());
const status = item.status || item.return_status || '已預約登記';
const note = item.note || item.memo || '';
result.push([date, category, itemName, teacher, periods, status, regTime, note]);
});
return result;
}
/**
* 合併寫入試算表,避免重複插入相同資料
*/
function mergeBookingsIntoSheet(sheet, rows) {
if (!rows || rows.length === 0) return 0;
const lastRow = sheet.getLastRow();
let existingKeys = new Set();
if (lastRow > 1) {
const existingData = sheet.getRange(2, 1, lastRow - 1, 5).getValues();
existingData.forEach(r => {
// 複合鍵: 日期_類別_品項_借用人_節次
const key = `${r[0]}_${r[1]}_${r[2]}_${r[3]}_${r[4]}`;
existingKeys.add(key);
});
}
const newRows = [];
rows.forEach(r => {
const key = `${r[0]}_${r[1]}_${r[2]}_${r[3]}_${r[4]}`;
if (!existingKeys.has(key)) {
newRows.push(r);
existingKeys.add(key);
}
});
if (newRows.length > 0) {
sheet.getRange(sheet.getLastRow() + 1, 1, newRows.length, newRows[0].length).setValues(newRows);
}
return newRows.length;
}
// ============================================================================
// 🔍 核心查詢模組 1:資訊設備借用查詢
// ============================================================================
function handleEquipmentQuery(subQuery) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(CONFIG.SHEET_BOOKINGS);
if (!sheet || sheet.getLastRow() <= 1) {
return `💻 【資訊設備借用現況】\n目前系統中尚無設備借用紀錄。若需預約請洽資訊組!`;
}
const data = sheet.getDataRange().getValues();
const todayStr = formatDateStr(getTwNow());
const tomorrowStr = formatDateStr(new Date(getTwNow().getTime() + 24 * 3600 * 1000));
let targetDate = todayStr;
let targetDateLabel = '今日';
if (subQuery && (subQuery.includes('明天') || subQuery.includes('明日'))) {
targetDate = tomorrowStr;
targetDateLabel = '明日';
}
const matches = [];
for (let i = 1; i < data.length; i++) {
const row = data[i];
const rDate = String(row[0]).trim();
const rCat = String(row[1]).trim();
const rItem = String(row[2]).trim();
const rTeacher = String(row[3]).trim();
const rPeriods = String(row[4]).trim();
const rStatus = String(row[5]).trim();
if (rCat === '資訊設備') {
const matchDate = (rDate === targetDate);
const matchItem = !subQuery || rItem.toLowerCase().includes(subQuery.toLowerCase()) || subQuery.includes(rItem);
if (matchDate && matchItem) {
matches.push({ item: rItem, teacher: rTeacher, periods: rPeriods, status: rStatus });
}
}
}
let reply = `💻 【${targetDateLabel} (${targetDate}) 資訊設備借用明細】\n`;
if (matches.length === 0) {
reply += `目前此時段尚無人預約借用設備,若需使用歡迎及早登記!\n`;
} else {
reply += `共 ${matches.length} 筆借用預約:\n\n`;
matches.forEach((m, idx) => {
reply += `${idx + 1}. 【${m.item}】\n 👤 借用老師:${m.teacher}\n 🕒 借用節次:${m.periods}\n 📌 狀態:${m.status}\n\n`;
});
}
reply += `💡 溫馨提醒:課堂使用完畢請協助清點配件(充電線、觸控筆),並於放學前歸還至資訊組充電。`;
if (CONFIG.BOOKING_WEB_URL) {
reply += `\n🔗 線上借用登記系統:${CONFIG.BOOKING_WEB_URL}`;
}
return reply.trim();
}
// ============================================================================
// 🔍 核心查詢模組 2:校園場地借用查詢
// ============================================================================
function handleVenueQuery(subQuery) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(CONFIG.SHEET_BOOKINGS);
if (!sheet || sheet.getLastRow() <= 1) {
return `🏛️ 【校園場地借用現況】\n目前系統中尚無場地借用紀錄。若需預約請洽主管單位登記!`;
}
const data = sheet.getDataRange().getValues();
const todayStr = formatDateStr(getTwNow());
const tomorrowStr = formatDateStr(new Date(getTwNow().getTime() + 24 * 3600 * 1000));
let targetDate = todayStr;
let targetDateLabel = '今日';
if (subQuery && (subQuery.includes('明天') || subQuery.includes('明日'))) {
targetDate = tomorrowStr;
targetDateLabel = '明日';
}
const matches = [];
for (let i = 1; i < data.length; i++) {
const row = data[i];
const rDate = String(row[0]).trim();
const rCat = String(row[1]).trim();
const rItem = String(row[2]).trim();
const rTeacher = String(row[3]).trim();
const rPeriods = String(row[4]).trim();
const rStatus = String(row[5]).trim();
if (rCat === '校園場地') {
const matchDate = (rDate === targetDate);
const matchItem = !subQuery || rItem.toLowerCase().includes(subQuery.toLowerCase()) || subQuery.includes(rItem);
if (matchDate && matchItem) {
matches.push({ venue: rItem, teacher: rTeacher, periods: rPeriods, status: rStatus });
}
}
}
let reply = `🏛️ 【${targetDateLabel} (${targetDate}) 校園場地預約排程】\n`;
if (matches.length === 0) {
reply += `今日校園各專科教室與會議場地目前均無登記借用,尚有充裕空檔!\n`;
} else {
reply += `共 ${matches.length} 筆場地登記:\n\n`;
matches.forEach((m, idx) => {
reply += `${idx + 1}. 【${m.venue}】\n 👤 使用人:${m.teacher}\n 🕒 時段:${m.periods}\n 📌 狀態:${m.status}\n\n`;
});
}
reply += `💡 提醒:借用電腦教室、創客教室請於前一日領取鑰匙;使用後請務必關閉冷氣電源並上鎖。`;
return reply.trim();
}
// ============================================================================
// 🔍 核心查詢模組 3:個人借用紀錄與學期次數統計
// ============================================================================
function handleTeacherStatsQuery(teacherName) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(CONFIG.SHEET_BOOKINGS);
if (!sheet || sheet.getLastRow() <= 1) {
return `📊 查無借用資料庫記錄。`;
}
const data = sheet.getDataRange().getValues();
const semesterStart = CONFIG.SEMESTER_START_DATE || '2026-08-01';
let totalCount = 0;
const itemFrequency = {};
const recentRecords = [];
const todayStr = formatDateStr(getTwNow());
const todayBookings = [];
for (let i = 1; i < data.length; i++) {
const row = data[i];
const rDate = String(row[0]).trim();
const rCat = String(row[1]).trim();
const rItem = String(row[2]).trim();
const rTeacher = String(row[3]).trim();
const rPeriods = String(row[4]).trim();
const rStatus = String(row[5]).trim();
// 比對老師姓名(支援模糊匹配或包含關係)
if (rTeacher.includes(teacherName) || teacherName.includes(rTeacher)) {
// 判斷是否落在本學期範圍
if (rDate >= semesterStart) {
totalCount++;
itemFrequency[rItem] = (itemFrequency[rItem] || 0) + 1;
recentRecords.push({
date: rDate,
cat: rCat,
item: rItem,
periods: rPeriods,
status: rStatus
});
if (rDate === todayStr) {
todayBookings.push({ item: rItem, periods: rPeriods });
}
}
}
}
if (totalCount === 0) {
return `📊 【個人借用紀錄查詢・${teacherName} 老師】\n\n自本學期起 (${semesterStart}),查無 ${teacherName} 老師的設備或場地借用紀錄。\n\n若您有借用需求,可隨時洽詢資訊組或至線上系統登記!`;
}
// 排序借用頻率最高的品項
const sortedItems = Object.entries(itemFrequency)
.sort((a, b) => b[1] - a[1])
.map(([name, count]) => `• ${name}:共借用 ${count} 次`);
// 取最近 5 筆借用
recentRecords.sort((a, b) => b.date.localeCompare(a.date));
const top5Records = recentRecords.slice(0, 5);
let reply = `📊 【個人借用紀錄統計報告】\n👤 查詢教席:${teacherName} 老師\n📅 統計區間:本學期 (${semesterStart} 至今)\n\n` +
`📈 【學期借用總計】:共 ${totalCount} 次\n\n` +
`📦 【借用品項與次數排行】:\n${sortedItems.join('\n')}\n\n` +
`🕒 【最近 5 次借用軌跡】:\n`;
top5Records.forEach(r => {
reply += `• ${r.date}(${r.periods}):${r.item} [${r.status}]\n`;
});
if (todayBookings.length > 0) {
reply += `\n🔔 【今日預約提醒】:\n您今天有預約「${todayBookings.map(b => b.item + ' (' + b.periods + ')').join('、')}」,請記得至資訊組領取或按時前往使用喔!`;
}
return reply.trim();
}
// ============================================================================
// 🧠 Gemini 自然語言 RAG 生成回覆
// ============================================================================
function generateAiResponse(userQuery, userId) {
if (!CONFIG.GEMINI_API_KEY || CONFIG.GEMINI_API_KEY.includes('填入您的')) {
return `⚠️ 尚未完成 Gemini API Key 設定,請至程式碼 CONFIG 中填入金鑰。\n您仍可使用「#查設備」、「#查場地」、「#查個人 [姓名]」進行快速查詢!`;
}
// 1. 抓取近期即時借用狀態作為 Prompt 上下文
const liveBookingsContext = getLiveBookingsSummaryContext();
// 2. 抓取常見問題 FAQ 與規範
const faqContext = getFaqKnowledgeBaseContext();
const fullPrompt = `${getSystemInstruction()}
【即時借用系統現況(今日與近期預約)】:
${liveBookingsContext}
【校內設備與場地管理規範 FAQ】:
${faqContext}
【老師目前提問】:
${userQuery}
請根據以上即時資料與管理規範,以溫暖、清晰且專業的繁體中文口吻回答老師:`;
// 呼叫 Gemini API
const authorizedModels = getAuthorizedGoogleModels(CONFIG.GEMINI_API_KEY);
let lastError = null;
for (let i = 0; i < authorizedModels.length; i++) {
const modelName = authorizedModels[i];
try {
const url = `https://generativelanguage.googleapis.com/v1beta/models/${modelName}:generateContent?key=${CONFIG.GEMINI_API_KEY}`;
const payload = {
contents: [{ role: 'user', parts: [{ text: fullPrompt }] }],
generationConfig: {
temperature: 0.3,
maxOutputTokens: 900
}
};
const response = UrlFetchApp.fetch(url, {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload),
muteHttpExceptions: true
});
if (response.getResponseCode() === 200) {
const resJson = JSON.parse(response.getContentText());
if (resJson.candidates && resJson.candidates.length > 0) {
const rawText = resJson.candidates[0].content.parts[0].text;
return sanitizeLineFormat(rawText);
}
} else {
lastError = response.getContentText();
}
} catch (e) {
lastError = e.toString();
}
}
console.error('Gemini API Error:', lastError);
return `抱歉,AI 系統連線稍有延遲。您可以直接輸入「#查設備」或「#查場地」查看今日預約狀況,或洽詢 ${CONFIG.ADMIN_CONTACT}!`;
}
function getLiveBookingsSummaryContext() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(CONFIG.SHEET_BOOKINGS);
if (!sheet || sheet.getLastRow() <= 1) {
return '(目前借用總表中暫無預約登記)';
}
const data = sheet.getDataRange().getValues();
const todayStr = formatDateStr(getTwNow());
const rows = [];
for (let i = 1; i < data.length; i++) {
const row = data[i];
const rDate = String(row[0]).trim();
if (rDate >= todayStr) {
rows.push(`- 日期: ${rDate} | 類別: ${row[1]} | 項目: ${row[2]} | 借用人: ${row[3]} | 節次: ${row[4]} | 狀態: ${row[5]}`);
}
}
return rows.length > 0 ? rows.slice(0, 30).join('\n') : '(今日與後續日期目前皆尚無登記紀錄)';
}
function getFaqKnowledgeBaseContext() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(CONFIG.SHEET_FAQ);
if (!sheet || sheet.getLastRow() <= 1) {
return '(若無明載規範,以資訊組日常借用辦法為準)';
}
const data = sheet.getDataRange().getValues();
const faqs = [];
for (let i = 1; i < data.length; i++) {
if (data[i][0] && data[i][1]) {
faqs.push(`Q: ${data[i][0]}\nA: ${data[i][1]}`);
}
}
return faqs.join('\n\n');
}
// ============================================================================
// 📱 LINE 通訊輔助工具 (Reply / Loading Animation / Sanitizer)
// ============================================================================
function sendLineReply(replyToken, messageText) {
if (!CONFIG.LINE_ACCESS_TOKEN || CONFIG.LINE_ACCESS_TOKEN.includes('填入您的')) {
console.warn('LINE_ACCESS_TOKEN not configured.');
return;
}
const url = 'https://api.line.me/v2/bot/message/reply';
const payload = {
replyToken: replyToken,
messages: [{
type: 'text',
text: messageText
}]
};
UrlFetchApp.fetch(url, {
method: 'post',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + CONFIG.LINE_ACCESS_TOKEN
},
payload: JSON.stringify(payload),
muteHttpExceptions: true
});
}
function showLineLoading(chatId) {
if (!CONFIG.LINE_ACCESS_TOKEN || CONFIG.LINE_ACCESS_TOKEN.includes('填入您的') || !chatId) return;
try {
const url = 'https://api.line.me/v2/bot/chat/loading/start';
UrlFetchApp.fetch(url, {
method: 'post',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + CONFIG.LINE_ACCESS_TOKEN
},
payload: JSON.stringify({ chatId: chatId, loadingSeconds: 15 }),
muteHttpExceptions: true
});
} catch (e) {
// 略過 loading 失敗例外
}
}
function sanitizeLineFormat(text) {
if (!text) return '';
return text.replace(/\*\*(.*?)\*\*/g, '「$1」').trim();
}
function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function getTwNow() {
const now = new Date();
return new Date(now.getTime() + (8 * 3600 * 1000) + (now.getTimezoneOffset() * 60000));
}
function formatDateStr(date) {
const y = date.getFullYear();
const m = ('0' + (date.getMonth() + 1)).slice(-2);
const d = ('0' + date.getDate()).slice(-2);
return `${y}-${m}-${d}`;
}
function formatDateTimeStr(date) {
const timeStr = date.toTimeString().split(' ')[0];
return `${formatDateStr(date)} ${timeStr}`;
}
function isVenueName(name) {
return /(教室|中心|會議室|館|室|講堂|球場|操場|舞台)/i.test(name);
}
function getAuthorizedGoogleModels(apiKey) {
return ['gemini-2.5-flash', 'gemini-1.5-flash', 'gemini-1.5-pro'];
}
function logConversation(query, reply, userId) {
try {
const ss = SpreadsheetApp.getActiveSpreadsheet();
let logSheet = ss.getSheetByName(CONFIG.SHEET_LOGS);
if (!logSheet) {
logSheet = ss.insertSheet(CONFIG.SHEET_LOGS);
logSheet.appendRow(['紀錄時間', '使用者ID', '查詢內容', '回覆摘要']);
}
logSheet.appendRow([formatDateTimeStr(getTwNow()), userId, query, reply.substring(0, 150)]);
} catch (e) {
console.error('logConversation error:', e);
}
}
// ============================================================================
// 🚀 一鍵初始化試算表與種子資料 (initBookingSystemSheet)
// ============================================================================
function initBookingSystemSheet() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
// 1. 建立「借用記錄總表」
let bSheet = ss.getSheetByName(CONFIG.SHEET_BOOKINGS);
if (!bSheet) {
bSheet = ss.insertSheet(CONFIG.SHEET_BOOKINGS);
bSheet.appendRow(['借用日期', '類別', '品項/場地名稱', '借用人', '借用節次', '借用狀態', '登記時間', '備註說明']);
bSheet.getRange('A1:H1').setBackground('#0284c7').setFontColor('#ffffff').setFontWeight('bold');
// 注入完整示範種子資料
const seedData = getSampleBookingSeedData();
bSheet.getRange(2, 1, seedData.length, seedData[0].length).setValues(seedData);
}
// 2. 建立「設備清單」
let eqSheet = ss.getSheetByName(CONFIG.SHEET_EQUIPMENT);
if (!eqSheet) {
eqSheet = ss.insertSheet(CONFIG.SHEET_EQUIPMENT);
eqSheet.appendRow(['設備名稱', '總數量', '存放地點', '配件說明', '借用限制']);
eqSheet.getRange('A1:E1').setBackground('#0284c7').setFontColor('#ffffff').setFontWeight('bold');
eqSheet.appendRow(['iPad 行動學習車 A', '30 台', '資訊組', '附專用充電線與多孔充電車', '限校內上課使用']);
eqSheet.appendRow(['iPad 行動學習車 B', '30 台', '資訊組', '附專用充電線與多孔充電車', '限校內上課使用']);
eqSheet.appendRow(['Chromebook 移動車', '30 台', '教務處', '專用變壓器與車箱', '提供 Google Classroom 課程']);
eqSheet.appendRow(['教師教學筆記型電腦', '15 台', '資訊組', '附滑鼠與電源線', '當日歸還']);
eqSheet.appendRow(['Apple Pencil 觸控筆組', '30 支', '資訊組', '筆尖防磨套', '隨車或單獨借用']);
eqSheet.appendRow(['高畫質實物投影機', '4 台', '設備組', '附 HDMI 線', '隨借隨還']);
eqSheet.appendRow(['4K 活動錄影單眼相機', '2 組', '資訊組', '含腳架與麥克風', '大型活動請提前三日預約']);
}
// 3. 建立「場地清單」
let vSheet = ss.getSheetByName(CONFIG.SHEET_VENUES);
if (!vSheet) {
vSheet = ss.insertSheet(CONFIG.SHEET_VENUES);
vSheet.appendRow(['場地名稱', '容納人數', '所在位置', '場地設備', '負責單位']);
vSheet.getRange('A1:E1').setBackground('#0284c7').setFontColor('#ffffff').setFontWeight('bold');
vSheet.appendRow(['電腦教室 (一)', '45 人', '圖資大樓 3F', '40台PC、電子白板、廣播系統', '資訊組']);
vSheet.appendRow(['電腦教室 (二)', '45 人', '圖資大樓 3F', '40台PC、高畫質投影機', '資訊組']);
vSheet.appendRow(['創客自造教室', '36 人', '工藝大樓 1F', '3D列印機、雷雕機、手工具', '設備組']);
vSheet.appendRow(['第一會議室', '30 人', '行政大樓 2F', '視訊會議設備、大型投影幕', '總務處']);
vSheet.appendRow(['階梯視聽教室', '120 人', '學生活動中心 2F', '專業音響、雙投影布幕、無線麥克風', '學務處']);
vSheet.appendRow(['室內活動中心體育館', '800 人', '活動中心 1F', '籃球場、羽球場、音響控制台', '體育組']);
}
// 4. 建立「借用規章與常見問答 FAQ」
let faqSheet = ss.getSheetByName(CONFIG.SHEET_FAQ);
if (!faqSheet) {
faqSheet = ss.insertSheet(CONFIG.SHEET_FAQ);
faqSheet.appendRow(['問題或規範主題', '詳細內容說明', '主管分機']);
faqSheet.getRange('A1:C1').setBackground('#0284c7').setFontColor('#ffffff').setFontWeight('bold');
faqSheet.appendRow(['平板車借用與領取時間', '請於上課前 10 分鐘至資訊組簽名領取,課後放學前(16:30前)全數清點插電歸還。', '資訊組分機 215']);
faqSheet.appendRow(['電腦教室使用守則', '教室內全面禁止攜帶開水以外之飲食;下課前請引導學生正常關機並將鍵盤滑鼠歸位。', '資訊組分機 215']);
faqSheet.appendRow(['場地鑰匙借還規定', '專科教室與會議室鑰匙請於使用當天至守衛室或總務處領取,使用完畢確認門窗電源後立即交回。', '總務處分機 231']);
faqSheet.appendRow(['設備若有故障如何報修', '請於還件時向管理人員說明異常狀況,或至校內智慧報修系統填報財產編號。', '資訊組分機 215']);
faqSheet.appendRow(['跨日或週末借用辦法', '如因校隊集訓或校外競賽需跨週末借用,需事先簽會相關處室主任核准。', '教務處分機 211']);
}
// 5. 建立「查詢與操作紀錄」
let logSheet = ss.getSheetByName(CONFIG.SHEET_LOGS);
if (!logSheet) {
logSheet = ss.insertSheet(CONFIG.SHEET_LOGS);
logSheet.appendRow(['紀錄時間', '使用者ID', '查詢內容', '回覆摘要']);
logSheet.getRange('A1:D1').setBackground('#64748b').setFontColor('#ffffff').setFontWeight('bold');
}
return '✅ 借用系統試算表結構與示範資料庫已成功初始化完成!';
}
/**
* 取得示範借用記錄種子資料
*/
function getSampleBookingSeedData() {
const todayStr = formatDateStr(getTwNow());
const tomorrowStr = formatDateStr(new Date(getTwNow().getTime() + 24 * 3600 * 1000));
const yesterdayStr = formatDateStr(new Date(getTwNow().getTime() - 24 * 3600 * 1000));
const pastWeekStr = formatDateStr(new Date(getTwNow().getTime() - 7 * 24 * 3600 * 1000));
return [
[todayStr, '資訊設備', 'iPad 行動學習車 A', '陳乃誠', '第 1、2 節', '借用中', todayStr + ' 07:45', '國文科數位閱讀評量'],
[todayStr, '校園場地', '電腦教室 (一)', '林志強', '第 3、4 節', '已預約登記', todayStr + ' 08:10', '七年級資訊科技課'],
[todayStr, '資訊設備', 'Chromebook 移動車', '張雅晴', '第 5、6 節', '已預約登記', todayStr + ' 08:30', '八年級社會領域探究實作'],
[todayStr, '校園場地', '第一會議室', '教務主任', '第 7 節', '已預約登記', todayStr + ' 09:00', '領域召集人行政工作會議'],
[tomorrowStr, '校園場地', '階梯視聽教室', '學務處訓育組', '第 3、4 節', '已預約登記', todayStr + ' 09:30', '全校法治教育專題講座'],
[tomorrowStr, '資訊設備', '4K 活動錄影單眼相機', '陳乃誠', '第 3、4 節', '已預約登記', todayStr + ' 10:15', '支援視聽教室講座側錄紀錄'],
[yesterdayStr, '資訊設備', 'iPad 行動學習車 A', '陳乃誠', '第 5、6 節', '已歸還', yesterdayStr + ' 08:00', '國文課Padlet小組討論'],
[yesterdayStr, '校園場地', '電腦教室 (一)', '陳乃誠', '第 7 節', '已歸還', yesterdayStr + ' 08:00', '資訊科技自主學習'],
[pastWeekStr, '資訊設備', 'iPad 行動學習車 A', '陳乃誠', '第 2、3 節', '已歸還', pastWeekStr + ' 08:00', '閱讀素養評量'],
[pastWeekStr, '校園場地', '創客自造教室', '王建華', '第 5、6 節', '已歸還', pastWeekStr + ' 08:00', '生科雷射雕刻實作']
];
} 貼到 Apps Script 的 Code.gs 檔案
5. 貼上前端(index.html)
把前端程式貼到 Apps Script 的 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><?= schoolName ?> - <?= botName ?> 即時資訊看板</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;700;900&display=swap" rel="stylesheet">
<style>
body { font-family: 'Noto Sans TC', sans-serif; }
</style>
</head>
<body class="bg-slate-50 text-slate-800 min-h-screen flex flex-col">
<!-- Header -->
<header class="bg-gradient-to-r from-sky-700 via-sky-600 to-indigo-700 text-white shadow-lg sticky top-0 z-50">
<div class="max-w-6xl mx-auto px-4 py-4 flex flex-wrap items-center justify-between gap-4">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 rounded-xl bg-white/20 flex items-center justify-center text-xl backdrop-blur-sm shadow-inner">
<i class="fa-solid fa-laptop-file"></i>
</div>
<div>
<h1 class="text-xl font-bold tracking-tight"><?= schoolName ?> <?= botName ?></h1>
<p class="text-xs text-sky-100 flex items-center gap-1">
<span class="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span>
<?= unitName ?> 即時借用資料庫・今日日期:<?= todayStr ?>
</p>
</div>
</div>
<div class="flex items-center gap-2">
<a href="?action=sync" class="px-3 py-1.5 bg-white/10 hover:bg-white/20 border border-white/20 rounded-lg text-xs font-semibold flex items-center gap-1.5 transition">
<i class="fa-solid fa-rotate"></i> 立即同步 API
</a>
</div>
</div>
</header>
<!-- Main Content -->
<main class="max-w-6xl mx-auto px-4 py-8 flex-1 w-full space-y-8">
<!-- Hero / Intro -->
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-200/80 flex flex-col md:flex-row items-center justify-between gap-6">
<div class="space-y-2">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-sky-100 text-sky-800">
<i class="fa-solid fa-bell mr-1"></i> 全校教職員借用即時連線中心
</span>
<h2 class="text-2xl font-black text-slate-900">隨時隨地掌握教學設備與教室空間排程</h2>
<p class="text-sm text-slate-600 max-w-2xl leading-relaxed">
結合校內借用系統 API、Google 試算表與 LINE 官方帳號。老師可透過 LINE 隨手查平板車、筆電空檔、會議室與電腦教室登記,更可直接統計個人本學期借用頻率!
</p>
</div>
<div class="flex flex-col sm:flex-row gap-3 w-full md:w-auto">
<div class="bg-slate-50 border border-slate-200 rounded-xl p-4 text-center min-w-[140px]">
<div class="text-2xl font-bold text-sky-600"><i class="fa-solid fa-tablet-screen-button"></i></div>
<div class="text-xs font-medium text-slate-500 mt-1">資訊設備管理</div>
<div class="text-sm font-bold text-slate-800 mt-0.5">即時清點登記</div>
</div>
<div class="bg-slate-50 border border-slate-200 rounded-xl p-4 text-center min-w-[140px]">
<div class="text-2xl font-bold text-indigo-600"><i class="fa-solid fa-landmark"></i></div>
<div class="text-xs font-medium text-slate-500 mt-1">校園專科場地</div>
<div class="text-sm font-bold text-slate-800 mt-0.5">時段衝突防範</div>
</div>
</div>
</div>
<!-- 3 Pillars Grid -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- 1. 資訊設備借用 -->
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-200/80 flex flex-col justify-between hover:shadow-md transition">
<div>
<div class="w-12 h-12 rounded-xl bg-sky-100 text-sky-600 flex items-center justify-center text-xl mb-4">
<i class="fa-solid fa-boxes-stacked"></i>
</div>
<h3 class="text-lg font-bold text-slate-900 mb-2">1. 資訊設備借用查詢</h3>
<p class="text-xs text-slate-600 mb-4 leading-relaxed">
透過 LINE 直接輸入「#查設備」或「平板車今天第幾節有空」,立刻調出 iPad 車、Chromebook、相機與投影機借用名單。
</p>
<div class="bg-slate-50 rounded-lg p-3 text-xs text-slate-700 space-y-1 font-mono">
<div class="text-sky-700 font-bold">📲 LINE 常用指令:</div>
<div>• #查設備</div>
<div>• #設備 平板車</div>
<div>• 明天有人借相機嗎?</div>
</div>
</div>
</div>
<!-- 2. 場地借用查詢 -->
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-200/80 flex flex-col justify-between hover:shadow-md transition">
<div>
<div class="w-12 h-12 rounded-xl bg-indigo-100 text-indigo-600 flex items-center justify-center text-xl mb-4">
<i class="fa-solid fa-door-open"></i>
</div>
<h3 class="text-lg font-bold text-slate-900 mb-2">2. 校園場地借用查詢</h3>
<p class="text-xs text-slate-600 mb-4 leading-relaxed">
會議室、電腦教室(一/二)、階梯視聽教室、活動中心排程連線。預防衝堂,上課前早知鑰匙領取時間。
</p>
<div class="bg-slate-50 rounded-lg p-3 text-xs text-slate-700 space-y-1 font-mono">
<div class="text-indigo-700 font-bold">📲 LINE 常用指令:</div>
<div>• #查場地</div>
<div>• #場地 會議室</div>
<div>• 今天電腦教室有人借嗎?</div>
</div>
</div>
</div>
<!-- 3. 個人借用統計 -->
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-200/80 flex flex-col justify-between hover:shadow-md transition">
<div>
<div class="w-12 h-12 rounded-xl bg-emerald-100 text-emerald-600 flex items-center justify-center text-xl mb-4">
<i class="fa-solid fa-chart-pie"></i>
</div>
<h3 class="text-lg font-bold text-slate-900 mb-2">3. 個人學期借用統計</h3>
<p class="text-xs text-slate-600 mb-4 leading-relaxed">
老師輸入姓名即可查詢自本學期起累計借用次數、最常借用的設備分佈排行,以及最近 5 筆借用足跡。
</p>
<div class="bg-slate-50 rounded-lg p-3 text-xs text-slate-700 space-y-1 font-mono">
<div class="text-emerald-700 font-bold">📲 LINE 常用指令:</div>
<div>• #查個人 陳乃誠</div>
<div>• #我的借用 林志強</div>
<div>• 我這學期借了幾次設備?</div>
</div>
</div>
</div>
</div>
<!-- API Sync & Google Sheet Info Box -->
<div class="bg-gradient-to-br from-slate-900 to-slate-800 rounded-2xl p-6 text-white shadow-xl">
<div class="flex flex-col md:flex-row items-center justify-between gap-6">
<div class="space-y-2">
<div class="flex items-center gap-2">
<span class="px-2 py-0.5 rounded bg-sky-500/20 text-sky-300 text-xs font-mono font-semibold border border-sky-500/30">Auto Sync Engine</span>
<span class="text-xs text-slate-400">每日 07:00 定時排程自動運作</span>
</div>
<h3 class="text-xl font-bold">學校借用系統 API 自動橋接器</h3>
<p class="text-xs text-slate-300 max-w-xl leading-relaxed">
免去手動登入學校系統查名冊的繁瑣。系統每日清晨自動發送 GET 請求獲取當日及近期借用資料,並智能比對排除重複項寫入試算表,確保 LINE Bot 資料永遠最新!
</p>
</div>
<div class="flex gap-3">
<a href="?action=sync" class="px-4 py-2.5 bg-sky-600 hover:bg-sky-500 rounded-xl text-xs font-bold text-white shadow-lg transition flex items-center gap-2">
<i class="fa-solid fa-cloud-arrow-down"></i> 手動測試 API 同步
</a>
</div>
</div>
</div>
</main>
<!-- Footer -->
<footer class="bg-white border-t border-slate-200 py-6 mt-12 text-center text-xs text-slate-500">
<div class="max-w-6xl mx-auto px-4">
<p>© 2026 <?= schoolName ?> <?= unitName ?>・校園智慧借用小幫手 LINE Bot・採用 Google Apps Script 與 Gemini RAG 開源技術</p>
</div>
</footer>
</body>
</html>
貼到 Apps Script 的 index.html 檔案
6. 部署
部署成 Web App 後,就會拿到可分享網址。
- Apps Script 右上:部署 → 新部署
- 類型選:Web app
- 執行身分:你
- 存取權限:任何人
- 完成後複製網址
💬 評論區
老師們的交流與提問
登入 後即可發表評論