0. 作品介紹
封面
2. 資料表規則
請照下面規則建立分頁與欄位。
第1分頁: 班級經營與知識庫
A 欄: 領域分類
B 欄: 標題 / 主題
C 欄: 詳細說明內容與規範指引
D 欄: 啟用狀態
E 欄: 最後更新時間
📄 解析結果(自動表格)
分頁:班級經營與知識庫
| A 欄 | B 欄 | C 欄 | D 欄 | E 欄 |
|---|---|---|---|---|
| 領域分類 | 標題 / 主題 | 詳細說明內容與規範指引 | 啟用狀態 | 最後更新時間 |
3. 開啟 Apps Script
從試算表開 Apps Script,才能直接用 SpreadsheetApp 讀寫資料。
- 試算表上方選單:擴充功能 → Apps Script
- 刪掉預設的程式碼(或全部選取覆蓋)
-
保留/建立檔案:
- Code.gs
- index.html
4. 貼上後端(Code.gs)
把後端程式貼到 Apps Script 的 Code.gs。
/**
* ============================================================================
* 🏫 全國班級導師 LINE Bot AI 智慧小幫手・通用開源範本 (GAS + Gemini RAG)
* ============================================================================
* 專為全台中小學、高中職各班班級導師設計的 24 小時智慧親師小助理!
*
* 聚焦四大核心領域:
* 1. 📈 學習情勢與學習歷程:作業繳交、課堂進度、平板自主學習、補救教學
* 2. 📝 段考範圍與評量時程:各科考程表、命題範圍、命題重點、平時成績比例
* 3. 🛡️ 班級經營與生活常規:班規、榮譽積點制度、手機保管規範、幹部職責
* 4. 🎪 學校活動與親師溝通:學校日親師座談會、運動會、校外教學、請假留言
*
* 核心功能:
* • ⚡ Google 試算表即班級大腦:段考範圍、作業進度隨改隨生效,免工程師維護!
* • 💌 在線請假與提問留言簿:支援「#請假」、「#提問」、「#留言」,自動生成編號入表!
* • 🧠 未解問題自進化機制:家長特殊提問自動收集,導師批改作業空檔補上即可!
* • 📱 班級群組防洗版過濾:私訊即問即答;在班級家長群需 @小幫手 始發言,不干擾日常!
* • ⏳ LINE 原生輸入中載入動畫:防止家長因等待而重複點擊。
* • 🚀 一鍵初始化函式:initKnowledgeBaseSheet() 自動建立 12 組班級常見標準問答。
* ============================================================================
*/
const CONFIG = {
// --------------------------------------------------------------------------
// 1. 必填金鑰 (請至 Google AI Studio 與 LINE Developers 後台取得)
// --------------------------------------------------------------------------
GEMINI_API_KEY: '填入您的_GEMINI_API_KEY',
LINE_ACCESS_TOKEN: '填入您的_LINE_CHANNEL_ACCESS_TOKEN',
// --------------------------------------------------------------------------
// 2. 學校與班級基本設定 (自動組裝溫暖專業的導師 AI 小分身)
// --------------------------------------------------------------------------
SCHOOL_NAME: '光明國民中學', // 學校名稱(例:光明國中、竹光國中)
CLASS_NAME: '八年一班', // 班級名稱(例:七年三班、高一甲班)
TEACHER_NAME: '陳乃誠老師', // 班級導師姓名
BOT_NAME: '八一班級小幫手', // 機器人稱呼
CONTACT_PHONE: '導師辦公室分機 201', // 學校總機與分機
CONTACT_TIME: '平日 08:00~17:00 (課間或放學後)', // 方便通話時段
CLASS_WEBSITE_URL: '', // 班級網站或雲端聯絡簿 (可選)
// --------------------------------------------------------------------------
// 3. 可選擴充模組 (若無需求請維持預設空字串 '',系統將自動略過不報錯)
// --------------------------------------------------------------------------
CALENDAR_ID: '', // 班級公開 Google 行事曆 ID (段考、活動、班會)
CALENDAR_WEB_URL: '', // 線上行事曆瀏覽網址
// --------------------------------------------------------------------------
// 4. 管理後台與工作表設定
// --------------------------------------------------------------------------
ADMIN_PASSWORD: '', // 手機 LINE 寫入密碼(留空即免密碼模式)
SHEET_NAME: '班級經營與知識庫', // 班級知識庫主要儲存表
LOG_SHEET_NAME: '親師對話紀錄', // 提問對話紀錄表
LEAVE_SHEET_NAME: '學生請假留言簿', // 家長請假與提問留言表 (#請假 #提問)
UNANSWERED_SHEET_NAME: '待補充問題庫' // AI 未命中待補問題表
};
// ============================================================================
// 🤖 動態組裝導師 AI 小幫手人設 Prompt (System Instruction)
// ============================================================================
function getSystemInstruction() {
return `你是一位充滿愛心、專業、耐心且溝通溫暖的「${CONFIG.SCHOOL_NAME} ${CONFIG.CLASS_NAME} 班級 AI 小幫手」(班導師「${CONFIG.TEACHER_NAME}」的官方 AI 秘書)。
你的任務是協助 ${CONFIG.CLASS_NAME} 的家長與學生,清楚解答學習進度、作業與課業輔導、定期評量(段考)範圍與考程、班級經營規定、榮譽獎勵辦法、學生請假以及學校重大活動日程。
【基本資訊與導師聯絡管道】:
- 學校與班級:${CONFIG.SCHOOL_NAME} ${CONFIG.CLASS_NAME}
- 班級導師:${CONFIG.TEACHER_NAME}
- 導師分機:${CONFIG.CONTACT_PHONE}
- 適合通話時段:${CONFIG.CONTACT_TIME}
${CONFIG.CLASS_WEBSITE_URL ? '- 班級專區/聯絡簿網址:' + CONFIG.CLASS_WEBSITE_URL : ''}
【回答原則與排版規範(極重要)】:
1. 一律使用台灣繁體中文,口吻親切溫和、誠懇同理,像老師對家長面對面聊天般自然。
2. 常用台灣中小學校園生活術語:段考、定期評量、聯絡簿、第八節課輔、因材網、平板、班規、請假、病假、事假、晨讀、午休、打掃、運動會、親師座談會。
3. 【嚴格 LINE 排版規範】:
- 嚴禁使用 Markdown 粗體星號(絕對不要輸出 **粗體** 或 *斜體* 或 ### 標題 或 - 破折號清單)!
- 強調重點請使用【...】或「...」。
- 清單一律使用 Emoji 圖示或數字(如 📚、📝、⏰、🎒、🟢、💡、👉、📅、🏢、•、1. 2. 3.)並適度空行,方便家長在手機 LINE 上舒適閱覽。
4. 【請假重要提示】:
- 若家長要在 LINE 登記請假,請主動提醒格式:輸入「#請假 [座號/姓名] [假別與事由]」,系統會自動建立紀錄並通知老師!
5. 【個別學生隱私與重大事件】:
- 涉及個別學生個人的家庭隱私、特殊身心身分或特定個案輔導狀況,請委婉告知家長:「因涉及孩子個人學習隱私,建議請於 ${CONFIG.CONTACT_TIME} 致電 ${CONFIG.CONTACT_PHONE} 與 ${CONFIG.TEACHER_NAME} 親自通話討論,親師共同守護孩子的成長!」
6. 若知識庫查無特定資訊,請誠懇告知家長,並主動引導家長輸入「#提問 [問題內容]」或於聯絡簿留言。`;
}
// ============================================================================
// 🌐 Web 介面與 LINE Webhook 入口
// ============================================================================
function doGet(e) {
return HtmlService.createHtmlOutputFromFile('index')
.setTitle(`${CONFIG.SCHOOL_NAME} ${CONFIG.CLASS_NAME} 班級小幫手 - 管理後台`)
.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);
}
// 2. 群組過濾:在班級家長群中,需 Tag @小幫手 或提及關鍵字才回應,避免干擾親師群
if (isGroup) {
const escapedBot = escapeRegExp(CONFIG.BOT_NAME);
const escapedTeacher = escapeRegExp(CONFIG.TEACHER_NAME);
const triggerPattern = new RegExp(`(@小幫手|班級小幫手|小幫手|導師小幫手|@${escapedBot}|${escapedBot}|${escapedTeacher})`, 'i');
if (!triggerPattern.test(userMessage) && !userMessage.startsWith('#')) {
return ContentService.createTextOutput('OK');
}
}
// 3. 檢查是否為學生請假指令 (#請假 ...)
if (/^(#請假|請假)\s*/.test(userMessage)) {
const leaveReply = handleLeaveCommand(userMessage, userId);
sendLineReply(replyToken, leaveReply);
logConversation(userMessage, leaveReply, userId);
return ContentService.createTextOutput('OK');
}
// 4. 檢查是否為家長在線提問/留言指令 (#提問, #留言, #聯絡 ...)
if (/^(#提問|提問|#留言|留言|#聯絡|聯絡)\s*/.test(userMessage)) {
const inquiryReply = handleInquiryCommand(userMessage, userId);
sendLineReply(replyToken, inquiryReply);
logConversation(userMessage, inquiryReply, userId);
return ContentService.createTextOutput('OK');
}
// 5. 檢查是否為導師/管理員指令 (#查詢請假, #查詢提問, #記住, #查詢)
if (userMessage.startsWith('#')) {
const adminReply = handleAdminCommand(userMessage);
sendLineReply(replyToken, adminReply);
return ContentService.createTextOutput('OK');
}
// 6. 一般家長提問:執行 RAG 知識庫比對與 Gemini 智慧回覆
const cleanPattern = new RegExp(`(@小幫手|班級小幫手|導師小幫手|小幫手|@${escapeRegExp(CONFIG.BOT_NAME)}|${escapeRegExp(CONFIG.BOT_NAME)}|${escapeRegExp(CONFIG.TEACHER_NAME)})`, 'gi');
const cleanQuery = userMessage.replace(cleanPattern, '').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 handleLeaveCommand(text, userId) {
const content = text.replace(/^(#請假|請假)\s*/, '').trim();
if (!content) {
return `📝 【${CONFIG.CLASS_NAME}・家長線上請假格式說明】\n\n` +
'請依下列格式輸入:\n' +
'👉 #請假 [學生座號或姓名] [請假日期] [假別與事由]\n\n' +
'💡 範例:\n' +
'• #請假 05號李小明 9/15(一) 因發燒就醫請病假一天\n' +
'• #請假 王小華 9/20上午因家庭事由請事假半天\n\n' +
`系統將自動建立請假登記單,並即時通知 ${CONFIG.TEACHER_NAME}!`;
}
const now = new Date();
const twTimeStr = Utilities.formatDate(now, 'GMT+8', 'yyyy/MM/dd HH:mm');
const leaveId = 'L' + Utilities.formatDate(now, 'GMT+8', 'MMddHHmm');
try {
const ss = SpreadsheetApp.getActiveSpreadsheet();
let sheet = ss.getSheetByName(CONFIG.LEAVE_SHEET_NAME);
if (!sheet) {
sheet = ss.insertSheet(CONFIG.LEAVE_SHEET_NAME);
sheet.appendRow(['登記單號', '登記時間', '類別', '家長ID', '學生與請假事由內容', '處理狀態', '導師回覆備註']);
sheet.getRange('A1:G1').setBackground('#0f766e').setFontColor('#ffffff').setFontWeight('bold');
}
sheet.appendRow([leaveId, twTimeStr, '學生請假', userId, content, '⏳ 待導師確認', '']);
return `📝 【${CONFIG.CLASS_NAME}・請假通報已受理】\n\n` +
`• 請假單號:${leaveId}\n` +
`• 通報時間:${twTimeStr}\n` +
`• 請假內容:${content}\n\n` +
`已同步登記至「${CONFIG.LEAVE_SHEET_NAME}」,並通知 ${CONFIG.TEACHER_NAME}!\n` +
`溫馨提醒:若為連續病假 3 天以上,回校時請記得檢附就醫收據或診斷證明書。祝孩子早日康復!`;
} catch (err) {
console.error('handleLeaveCommand error:', err);
return `⚠️ 抱歉,線上請假登記失敗,請直接致電學校轉分機:${CONFIG.CONTACT_PHONE} 與導師聯絡。`;
}
}
function handleInquiryCommand(text, userId) {
const content = text.replace(/^(#提問|提問|#留言|留言|#聯絡|聯絡)\s*/, '').trim();
if (!content) {
return `💌 【${CONFIG.CLASS_NAME}・家長在線留話登記】\n\n` +
'若有特定事項需請導師了解,請依下列格式輸入:\n' +
'👉 #提問 [家長稱謂與具體事項]\n\n' +
'💡 範例:\n' +
'• #提問 05號小明媽媽:想了解孩子近期上課分組表現\n' +
'• #提問 12號大華爸爸:想請教英文科單字自主學習推薦資源\n\n' +
`系統將自動轉交給 ${CONFIG.TEACHER_NAME},老師將在空堂或放學後回覆!`;
}
const now = new Date();
const twTimeStr = Utilities.formatDate(now, 'GMT+8', 'yyyy/MM/dd HH:mm');
const inquiryId = 'Q' + Utilities.formatDate(now, 'GMT+8', 'MMddHHmm');
try {
const ss = SpreadsheetApp.getActiveSpreadsheet();
let sheet = ss.getSheetByName(CONFIG.LEAVE_SHEET_NAME);
if (!sheet) {
sheet = ss.insertSheet(CONFIG.LEAVE_SHEET_NAME);
sheet.appendRow(['登記單號', '登記時間', '類別', '家長ID', '學生與請假事由內容', '處理狀態', '導師回覆備註']);
sheet.getRange('A1:G1').setBackground('#0f766e').setFontColor('#ffffff').setFontWeight('bold');
}
sheet.appendRow([inquiryId, twTimeStr, '家長提問留言', userId, content, '⏳ 待導師回覆', '']);
return `💌 【${CONFIG.CLASS_NAME}・提問留言已收到】\n\n` +
`• 留言單號:${inquiryId}\n` +
`• 登記時間:${twTimeStr}\n` +
`• 留言內容:${content}\n\n` +
`感謝家長的關心!${CONFIG.TEACHER_NAME} 將於空堂或課後查覆。若有急事亦可於 ${CONFIG.CONTACT_TIME} 致電分機 ${CONFIG.CONTACT_PHONE}。`;
} catch (err) {
console.error('handleInquiryCommand error:', err);
return `⚠️ 抱歉,留言登記暫時發生異常,請直接透過聯絡簿留言或致電分機 ${CONFIG.CONTACT_PHONE}。`;
}
}
// ============================================================================
// 📅 班級公開行事曆查詢 (可選模組)
// ============================================================================
function getClassCalendarSummary(query) {
if (!CONFIG.CALENDAR_ID || CONFIG.CALENDAR_ID.trim() === '') {
return '';
}
try {
const calendar = CalendarApp.getCalendarById(CONFIG.CALENDAR_ID.trim());
if (!calendar) return '';
const now = new Date();
const twNow = new Date(now.getTime() + (8 * 3600 * 1000) + (now.getTimezoneOffset() * 60000));
let daysAhead = 14;
let daysPast = 1;
if (query.indexOf('整學期') !== -1 || query.indexOf('月曆') !== -1 || query.indexOf('下個月') !== -1) {
daysAhead = 45;
} else if (query.indexOf('段考') !== -1 || query.indexOf('考試') !== -1 || query.indexOf('學校日') !== -1) {
daysAhead = 21;
}
const startTw = new Date(twNow.getTime() - (daysPast * 24 * 3600 * 1000));
const endTw = new Date(twNow.getTime() + (daysAhead * 24 * 3600 * 1000));
const events = calendar.getEvents(startTw, endTw);
if (!events || events.length === 0) return '';
const startStr = formatDateStr(startTw);
const endStr = formatDateStr(endTw);
const eventList = [];
for (let i = 0; i < events.length; i++) {
const e = events[i];
const sum = e.getTitle().trim();
if (!sum) continue;
if (e.isAllDayEvent()) {
const sTime = new Date(e.getStartTime().getTime() + (8 * 3600 * 1000) + (e.getStartTime().getTimezoneOffset() * 60000));
const dateFmt = formatDateStr(sTime);
const wStr = ['日', '一', '二', '三', '四', '五', '六'][sTime.getDay()];
eventList.push({
dateStr: dateFmt,
display: `• 【${String(sTime.getMonth()+1).padStart(2, '0')}/${String(sTime.getDate()).padStart(2, '0')} (${wStr})】:${sum}`
});
} else {
const s = new Date(e.getStartTime().getTime() + (8 * 3600 * 1000) + (e.getStartTime().getTimezoneOffset() * 60000));
const dateFmt = formatDateStr(s);
const wStr = ['日', '一', '二', '三', '四', '五', '六'][s.getDay()];
const timeFmt = `${String(s.getHours()).padStart(2, '0')}:${String(s.getMinutes()).padStart(2, '0')}`;
eventList.push({
dateStr: dateFmt,
display: `• 【${String(s.getMonth()+1).padStart(2, '0')}/${String(s.getDate()).padStart(2, '0')} (${wStr}) ${timeFmt}】:${sum}`
});
}
}
eventList.sort((a, b) => a.dateStr.localeCompare(b.dateStr));
if (eventList.length === 0) {
return `【班級重要日程】:在 ${startStr} 至 ${endStr} 期間查無特定登記活動${CONFIG.CALENDAR_WEB_URL ? '(可至線上行事曆 ' + CONFIG.CALENDAR_WEB_URL + ' 查看)' : ''}。`;
}
return `📅 【${CONFIG.CLASS_NAME}班級與學校重要行事明細 (${startStr} ~ ${endStr},共 ${eventList.length} 項)】:\n` +
eventList.map(e => e.display).join('\n') +
(CONFIG.CALENDAR_WEB_URL ? `\n\n【完整線上行事曆】:${CONFIG.CALENDAR_WEB_URL}` : '');
} catch(e) {
console.error('getClassCalendarSummary error:', e);
return '';
}
}
// ============================================================================
// 📚 班級知識庫管理核心函式
// ============================================================================
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
});
}
}
return list;
} catch (e) {
console.warn('getKnowledgeBaseData error:', e);
return [];
}
}
function handleAdminCommand(text) {
const parts = text.split(' ');
const cmd = parts[0];
if (cmd === '#查詢請假' || cmd === '#查詢提問' || cmd === '#查詢留言') {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(CONFIG.LEAVE_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][5] && values[i][5].toString().includes('待')) {
pending.push(`• [${values[i][0]}] ${values[i][2]} (${values[i][1]}):${values[i][4]}`);
}
}
if (pending.length === 0) return '🎉 太棒了!目前所有家長請假與留言皆已處理完畢,無待辦事項!';
return `📝 【待處理請假與留言清單 (${pending.length} 筆)】:\n` + pending.slice(0, 8).join('\n');
}
if (cmd === '#查詢') {
const kw = parts.slice(1).join(' ');
if (!kw) return '請輸入要查詢的關鍵字,例如:#查詢 段考';
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 '❌ 請提供要記住的內容,例如:#記住 明天數學小考改至週四早自習';
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(CONFIG.SHEET_NAME);
const title = content.slice(0, 15);
sheet.appendRow(['臨時班級快訊', title, content, '啟用', new Date()]);
return `✅ 已成功寫入班級知識庫!\n【分類】臨時班級快訊\n【項目】${title}\n【內容】${content}`;
}
return '💡 導師常用指令:\n• #請假 [座號/事由]\n• #提問 [內容]\n• #查詢請假\n• #查詢 [關鍵字]\n• #記住 [快訊內容]';
}
// ============================================================================
// 🧠 RAG 知識庫比對與 Gemini AI 生成
// ============================================================================
function generateAiResponse(userQuery, userId) {
if (!CONFIG.GEMINI_API_KEY || CONFIG.GEMINI_API_KEY.includes('填入您的')) {
return '⚠️ 系統提示:尚未在 Code.gs 中設定 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');
// 1. 判斷是否需要擴充模組資訊(行事曆)
let liveCalendarContext = '';
if (CONFIG.CALENDAR_ID && /(行事曆|段考|考試|學校日|運動會|校慶|放假|露營|畢業旅行|時程|下週|這週|明天|今天)/i.test(userQuery)) {
liveCalendarContext = getClassCalendarSummary(userQuery);
}
const prompt = getSystemInstruction() + '\n\n' +
'以下是「' + CONFIG.SCHOOL_NAME + ' ' + CONFIG.CLASS_NAME + ' 班級經營與課業指引知識庫」:\n' +
'--------------------------------\n' +
contextText + '\n' +
'--------------------------------\n' +
(liveCalendarContext ? ('\n【📅 即時連線班級重要日程】:\n' + liveCalendarContext + '\n--------------------------------\n') : '') +
'\n家長或同學的提問是:「' + userQuery + '」\n\n' +
'請依據上述知識庫、導師人設與回答規範,給出溫暖體貼、條理清晰、易於閱讀的解答:';
const payload = {
contents: [{ parts: [{ text: prompt }] }],
generationConfig: { temperature: 0.2 }
};
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);
// 🧠 若 AI 查無答案,自動收集進待補充清單
if (finalReply.includes('目前沒有這項特定資料') || (finalReply.includes('致電') && finalReply.includes(CONFIG.TEACHER_NAME))) {
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 '⚠️ 抱歉,AI 服務連線稍有延遲,請稍候片刻再試一次,或直接致電 ' + CONFIG.TEACHER_NAME + '(' + CONFIG.CONTACT_PHONE + ')。';
}
function getAuthorizedGoogleModels(apiKey) {
const cache = CacheService.getScriptCache();
const cached = cache.get('CLASS_GEMINI_MODELS_V1');
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/', ''));
}
}
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('CLASS_GEMINI_MODELS_V1', JSON.stringify(valid), 86400);
return valid;
}
}
} catch (e) {
console.warn('getAuthorizedGoogleModels error:', e);
}
return ['gemini-2.5-flash', 'gemini-1.5-flash', 'gemini-1.5-flash-latest'];
}
// ============================================================================
// 🚀 工具函式庫 (LINE 訊息傳送、排版美化、日期格式化)
// ============================================================================
function sendLineReply(replyToken, messageText) {
if (!CONFIG.LINE_ACCESS_TOKEN || CONFIG.LINE_ACCESS_TOKEN.includes('填入您的')) {
console.warn('LINE_ACCESS_TOKEN 未設定');
return;
}
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 showLineLoading(chatId) {
if (!chatId || !CONFIG.LINE_ACCESS_TOKEN || CONFIG.LINE_ACCESS_TOKEN.includes('填入您的')) 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: 15
}),
muteHttpExceptions: true
});
} catch (e) {
console.warn('showLineLoading failed:', e);
}
}
function sanitizeLineFormat(text) {
if (!text) return '';
return text
.replace(/\*\*(.*?)\*\*/g, '【$1】')
.replace(/\*(.*?)\*/g, '$1')
.replace(/^#+\s+/gm, '')
.replace(/^-\s+/gm, '• ')
.trim();
}
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function formatDateStr(date) {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
}
function logConversation(query, reply, userId) {
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', '提問內容', '小幫手回覆摘要']);
sheet.getRange('A1:D1').setBackground('#0f766e').setFontColor('#ffffff').setFontWeight('bold');
}
sheet.appendRow([new Date(), userId, query, reply.slice(0, 150)]);
} catch (e) {
console.warn('logConversation error:', e);
}
}
function logUnansweredQuestion(query, aiReply, 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('#b91c1c').setFontColor('#ffffff').setFontWeight('bold');
}
sheet.appendRow([new Date(), userId, query, aiReply.slice(0, 80), '', '⏳ 待導師補充']);
} catch (e) {
console.warn('logUnansweredQuestion error:', e);
}
}
// ============================================================================
// 🚀 一鍵初始化函式:建立 12 組全台中小學班級常見標準題庫
// ============================================================================
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 defaultKnowledge = [
// ------------------------------------------------------------------------
// A. 學習情勢與學習歷程 (3 題)
// ------------------------------------------------------------------------
[
"學習情勢與歷程",
"每日作業繳交、家庭聯絡簿簽閱與缺交補交機制",
`1. 【聯絡簿簽閱時間】:請家長每日晚間主動查閱孩子的家庭聯絡簿,確認各科作業清單、小考成績並於「家長簽名欄」簽章。
2. 【每日作業繳交時限】:翌日早自習 07:45 前由各科小老師收齊送交各任課老師;若遇缺交,幹部將登記於「每日作業檢核表」。
3. 【補繳作業規範】:當日午休前完成補繳得予銷單;累計缺繳達 3 次未繳者,將於聯絡簿紅字提醒並由導師利用午休或放學後留校個別輔導補齊。
💡 家長若想了解孩子特定學科繳交狀況,可直接於聯絡簿留言或在 LINE 輸入「#提問 [學生姓名] 查詢作業」。`,
"啟用",
new Date()
],
[
"學習情勢與歷程",
"課堂學習進度規劃與生生用平板自主學習指引",
`1. 【各科教學進度】:學校依教育部 108 課綱與學期排課進度授課,各科任課教師每週會將當週教學進度及評量預告更新於班級聯絡簿。
2. 【生生用平板數位學習】:
• 本班配合教育部「推動中小學數位學習精進方案」,課堂導入因材網、Cool English、均一教育平台與學習吧。
• 學生皆有專屬教育雲端帳號(OpenID),放學返家亦可使用個人電腦或平板登入進行課後非同步複習。
3. 【自主學習追蹤】:導師會定期檢視學生因材網學習任務完成率,針對概念弱點進行個別化診斷提醒。`,
"啟用",
new Date()
],
[
"學習情勢與歷程",
"第八節課後輔導(課藝活動)與教育部學習扶助(補救教學)實施辦法",
`1. 【第八節課後輔導】:
• 實施時段:週一至週四 16:20 ~ 17:05(17:10 放學)。
• 實施原則:秉持「自由參加、學生自願、家長同意」原則,於學期初發放家長同意書辦理。
• 教學重點:著重國英數社自等主科加深加廣、會考題型解題思維與統整演練。
2. 【教育部學習扶助班(補救教學)】:
• 針對前次段考或學力檢測特定科目未達基礎標準之學生,由學校免費開設 6~10 人小班制精緻補救課程。
• 經家長同意後,由專任師資針對單元迷思概念重點補強,學費全額由教育部補助。`,
"啟用",
new Date()
],
// ------------------------------------------------------------------------
// B. 段考範圍與評量時程 (3 題)
// ------------------------------------------------------------------------
[
"段考範圍與日程",
"第一次定期評量(段考)時程、考程表與放學作息調整",
`1. 【考試日期】:學期第 8~9 週(依學校教務處行事曆正式公告為準,通常為週二至週三)。
2. 【考試作息調整】:
• 每日 07:30 ~ 07:50 晨間到校安靜自修,08:15 第一節開始考。
• 段考期間全校暫停第八節課輔,每日下午 15:55 準時放學(未留校溫書者此時離校)。
3. 【應考必備文具】:
• 黑色 2B 鉛筆、橡皮擦(畫卡用,禁止使用非 2B 筆劃卡)。
• 黑色或藍色原子筆、修正帶(國文手寫、數學計算過程必備)。
• 嚴禁隨身攜帶手機或具聯網通訊功能之智慧手錶進入試場。`,
"啟用",
new Date()
],
[
"段考範圍與日程",
"各科定期評量(段考)命題範圍與命題重點一覽",
`【本學期第一次定期評量範圍】:
• 【國文】:第 1 課至第 4 課(含語文常識一、課本注釋、成語選粹與論語選讀)。
• 【英語】:Unit 1 ~ Unit 3(含 Review 1、單字片語、課文文法重點與英聽測驗)。
• 【數學】:第 1 章 乘法公式與多項式 ~ 第 2 章 平方根與畢氏定理。
• 【自然/理化】:第 1 章 基本測量 ~ 第 2 章 物質的世界(含實驗操作題型)。
• 【社會(歷/地/公)】:
- 歷史:第 1 ~ 2 章
- 地理:第 1 ~ 2 章(地形與氣候)
- 公民:第 1 ~ 2 章(個人與家庭生活常規)
💡 各科任課老師已發放段考複習卷,請督促孩子提早擬定溫書計畫!`,
"啟用",
new Date()
],
[
"段考範圍與日程",
"學生學期成績評量計算標準與平時多元評量計分方式",
`1. 【學期成績配分比例】:
• 定期評量(三次段考平均):佔 40%
• 平時成績(多元評量總和):佔 60%
2. 【平時多元評量包含項目】:
• 各單元紙筆測驗與隨堂小考:佔 30%
• 課堂作業、習作與學習單繳交品質:佔 30%
• 課堂學習態度、分組合作專案、口頭發表與上課參與度:佔 40%
💡 導師特別提醒:平時作業認真書寫、準時繳交,以及課堂積極參與,佔比高達六成,請同學切勿只在段考前臨時抱佛腳!`,
"啟用",
new Date()
],
// ------------------------------------------------------------------------
// C. 班級經營與生活常規 (3 題)
// ------------------------------------------------------------------------
[
"班級經營與常規",
"班級公約、每日晨間作息與生活常規守則",
`1. 【晨間作息規範】:
• 07:30 ~ 07:50:到校時間,進教室後立即交齊作業至小老師桌面。
• 07:50 ~ 08:25:晨讀靜息時間或導師時間,教室保持安靜,嚴禁吃早餐、聊天或走動(請於 07:45 前用畢早餐)。
2. 【核心班級公約】:
• 【自律尊重】:尊重師長、友愛同學,禁止使用言語嘲諷、肢體碰撞或網路霸凌。
• 【環境維護】:個人座位周遭落實「垃圾不落地」,每日放學徹底清空抽屜,桌椅對齊地磚線。
• 【課堂專注】:鐘聲響畢 1 分鐘內就座,擺妥該節課本與文具,非當節課物品一律收入抽屜。`,
"啟用",
new Date()
],
[
"班級經營與常規",
"班級榮譽點數(集點制度)與正向管教獎懲辦法",
`1. 【個人與小組積點獎勵】:
• 個人加分:段考/小考進步、作業特優、主動熱心公德、擔任值日幹部盡責、代表班級參賽。
• 小組競賽:每週依課堂回答、作業全齊率、整潔評比累計「組別榮譽榜」。
2. 【榮譽點數兌換機制】:
• 累積滿 20 點:可兌換「座位優先自選權一週」或「小文具點心獎勵」。
• 累積滿 50 點:於學期末親師座談會獲頒「班級品學優良榮譽狀」並記嘉獎肯定。
3. 【違規叮嚀】:遲到、作業缺交、課堂喧嘩將予以提醒扣點或執行班級公共服務勞動。`,
"啟用",
new Date()
],
[
"班級經營與常規",
"學生在校手機與智慧手錶保管規範、防潮鎖櫃管理辦法",
`1. 【手機攜帶申請】:學生攜帶手機入校須由家長填具「學生在校攜帶行動電話申請書」備查。
2. 【班級養機場管理原則】:
• 每日早自習 07:45 前,副班長統一督促全班將手機關機,收齊鎖入「班級防潮手機鎖櫃」。
• 手機鑰匙由導師親自保管,放學前打掃完畢後統一發還。
3. 【緊急聯絡方式】:在校期間若有突發緊急事情需聯繫家長,可經師長同意後借用導師辦公室電話或至教務處公用電話聯絡。`,
"啟用",
new Date()
],
// ------------------------------------------------------------------------
// D. 學校活動與親師溝通 (3 題)
// ------------------------------------------------------------------------
[
"學校活動與溝通",
"學生請假手續(病假/事假)、差勤回報與當日通報管道",
`1. 【當日臨時病假】:
• 請家長於早上 07:45 前,直接在 LINE 輸入:「#請假 [座號姓名] [病假事由]」,系統會自動登記並發送給導師。
• 或可於 07:30 ~ 08:00 致電學校總機轉 ${CONFIG.CONTACT_PHONE} 告知導師。
2. 【事前事假手續】:請於 3 天前由家長於家庭聯絡簿填寫事由,或由學生至學務處領取正式請假單簽核。
3. 【銷假與就醫證明】:病假連續 3 天以上,依學務處規定需檢附醫療收據或就醫診斷證明以完成銷假程序。`,
"啟用",
new Date()
],
[
"學校活動與溝通",
"學校日(全校親師座談會)活動時程、座談主題與預約個別晤談",
`1. 【學校日辦理時間】:每學期第 3 週之週五晚間 18:30 ~ 21:00(請見學校紙本邀請函及行事曆)。
2. 【親師座談流程】:
• 18:30 ~ 19:15:全校校務與各處室願景報告(禮堂/線上直播)。
• 19:20 ~ 20:30:班級導師時間(導師班級經營理念、會考學力展望、各科教學進度介紹)。
• 20:30 ~ 21:00:班級家長代表(家長委員)推選與自由親師交流。
3. 【個別深度晤談預約】:若家長希望針對孩子的個別學習或情緒狀態進行深度對談,歡迎於聯絡簿留言預約空堂時段。`,
"啟用",
new Date()
],
[
"學校活動與溝通",
"本學期校園重大活動(校慶運動會、戶外教育/隔宿露營、家長志工招募)",
`1. 【全校運動會】:預計於 11 月中旬舉行,當日全天出缺席列入出勤考查,歡迎家長蒞校參觀為孩子加油打氣!
2. 【戶外教育/隔宿露營/畢業旅行】:
• 行前一至兩個月將發放完整家長同意書、攜帶物品清單、服裝規定與退費規範。
• 學生如有特殊體質(如氣喘、心臟病、特定藥物過敏),請務必於健康調查表中如實填報。
3. 【班級家長志工招募】:誠摯邀請熱心家長擔任班級家長代表、圖書志工或活動交通導護志工,共同為孩子的班級成長護航!`,
"啟用",
new Date()
]
];
defaultKnowledge.forEach(function(row) {
sheet.appendRow(row);
});
sheet.autoResizeColumns(1, 5);
console.log('🎉 班級導師小幫手標準知識庫初始化完成!已填入 ' + defaultKnowledge.length + ' 筆班級核心題庫。');
} 貼到 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>光明國民中學 八年一班 八一班級小幫手 - 班級管理後台</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; }
.code-scroll::-webkit-scrollbar { height: 6px; width: 6px; }
.code-scroll::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
</style>
</head>
<body class="bg-slate-100 text-slate-800 min-h-screen">
<!-- 頂部導航 -->
<header class="bg-indigo-950 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-indigo-500 flex items-center justify-center text-xl font-bold shadow-md">
<i class="fa-solid fa-chalkboard-user"></i>
</div>
<div>
<h1 class="text-lg font-bold">光明國民中學 八年一班 八一班級小幫手</h1>
<p class="text-xs text-indigo-200">八年一班・親師溝通與段考範圍管理後台</p>
</div>
</div>
<div class="flex items-center space-x-2">
<span class="px-3 py-1 bg-indigo-900 border border-indigo-700 rounded-full text-xs text-indigo-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-indigo-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-indigo-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>
</div>
</div>
</div>
<!-- 知識庫卡片展示區 -->
<div id="loading" class="text-center py-12 text-slate-400">
<i class="fa-solid fa-spinner fa-spin text-3xl mb-3 text-indigo-600"></i>
<p class="text-sm">正在載入試算表班級資料庫...</p>
</div>
<div id="cardGrid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5 hidden"></div>
<div id="emptyState" class="text-center py-16 bg-white rounded-2xl border border-slate-200 shadow-sm hidden">
<i class="fa-solid fa-folder-open text-4xl text-slate-300 mb-2"></i>
<p class="text-slate-500 text-sm">找不到符合搜尋條件的班級項目</p>
</div>
</main>
<script>
let rawData = [];
let currentCategory = 'ALL';
window.onload = function() {
google.script.run
.withSuccessHandler(renderData)
.withFailureHandler(function(err) {
document.getElementById('loading').innerHTML =
'<p class="text-rose-500 font-medium">載入失敗:' + err + '</p>';
})
.getKnowledgeBaseData();
};
function renderData(data) {
rawData = data || [];
document.getElementById('loading').classList.add('hidden');
document.getElementById('cardGrid').classList.remove('hidden');
filterCards();
}
function filterCategory(cat) {
currentCategory = cat;
document.querySelectorAll('.cat-btn').forEach(btn => {
if (btn.innerText.includes(cat) || (cat === 'ALL' && btn.innerText === '全部') || (btn.getAttribute('onclick') && btn.getAttribute('onclick').includes(cat))) {
btn.className = 'cat-btn px-3 py-1.5 rounded-lg bg-indigo-700 text-white font-medium';
} else {
btn.className = 'cat-btn px-3 py-1.5 rounded-lg bg-slate-100 text-slate-600 hover:bg-slate-200 font-medium';
}
});
filterCards();
}
function filterCards() {
const kw = document.getElementById('searchInput').value.trim().toLowerCase();
const grid = document.getElementById('cardGrid');
const empty = document.getElementById('emptyState');
grid.innerHTML = '';
const filtered = rawData.filter(item => {
const matchCat = currentCategory === 'ALL' || item.category.includes(currentCategory);
const matchKw = !kw || item.title.toLowerCase().includes(kw) || item.content.toLowerCase().includes(kw);
return matchCat && matchKw;
});
if (filtered.length === 0) {
empty.classList.remove('hidden');
return;
}
empty.classList.add('hidden');
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 hover:border-indigo-400 transition-all";
card.innerHTML = `
<div class="space-y-3">
<span class="inline-block px-2.5 py-1 rounded-md text-xs font-semibold bg-indigo-50 text-indigo-700 border border-indigo-200">
${escapeHtml(item.category)}
</span>
<h3 class="font-bold text-slate-900 text-base leading-snug">${escapeHtml(item.title)}</h3>
<div class="text-xs text-slate-600 whitespace-pre-line leading-relaxed code-scroll max-h-48 overflow-y-auto bg-slate-50 p-3 rounded-xl border border-slate-100">
${escapeHtml(item.content)}
</div>
</div>
<div class="pt-4 mt-3 border-t border-slate-100 flex items-center justify-between text-[11px] text-slate-400">
<span><i class="fa-solid fa-check text-emerald-500 mr-1"></i>試算表即時連動</span>
<span class="text-indigo-600 font-medium">Gemini RAG</span>
</div>
`;
grid.appendChild(card);
});
}
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
</script>
</body>
</html>
貼到 Apps Script 的 index.html 檔案
6. 部署
部署成 Web App 後,就會拿到可分享網址。
- Apps Script 右上:部署 → 新部署
- 類型選:Web app
- 執行身分:你
- 存取權限:任何人
- 完成後複製網址
💬 評論區
老師們的交流與提問
登入 後即可發表評論