🤖 AI 小幫手回答:
老師您好!目前時間是【第二節】(09:15-10:10),借用狀況如下:
📱 第九車:范慧蘭 老師
📱 第十車:楊千惠 老師
其餘第十一、十二、十四、十五車目前皆為空閒可借用喔!
🤖 AI 小幫手回答:
查核結果:第三節 (10:10-11:05) 的「第九車」目前無人登記借用,是空閒狀態!您可以直接至線上系統登記借用喔。
🤖 AI 小幫手回答:
今天 1樓卓越基地的登記紀錄:
🏫 借用人:文書組長
⏰ 借用節次:第二節、第三節
📝 會議事由:115學年度第1次全校會議
如何新增、修改後台知識庫資料?
打造好 AI 小幫手後,日後學校有新規定、新設備或新的常見問答時,您可以透過以下 3 種方式 隨時更新,完全不需要修改任何程式碼,也不用重新部署!
方法一:直接在 Google 試算表新增
打開試算表的「資訊組知識庫」工作表,在最底下一列填寫:
方法二:手機 LINE 對話一鍵記憶
組長在外巡堂或修設備時,直接在 LINE 對小幫手傳送指令:
Gemini 會自動結構化提取分類與標題,並自動追加一列寫入 Google 試算表!
方法三:透過 Web 控制台管理
點開部署產生的 Web App 網址(index.html 面板),您可以看到美麗的卡片清單:
- 依「8 大業務分類」快速切換標籤。
- 即時搜尋框,輸入關鍵字秒查內容。
- 查看各項目的狀態與 ID 編號。
新任資訊組長常見 8 大核心業務範例庫
整理好台灣中小學最常見的 8 大資訊業務分類,已全面整合平板車線上借用系統!
校園網路與帳號
- • 新進教師公務 Google/OpenID 開通
- • 校園 802.1X Wi-Fi 連線設定
- • eduroam 跨校漫遊帳號指引
數位教學與軟體
- • Office 365 / Windows 授權認證
- • Canva 教育版免費教師申請
- • Google Classroom 班級開通
數位研習增能管考
- • 教育部 A1、A2、A3 研習規範
- • edu 磨課師+ 線上修課 SOP
- • 全國在職進修網時數查詢
校務系統與行政
- • 1Campus / 智慧校園導師開通
- • 公文線上簽核系統登入
- • 差勤請假系統權限申請
平板與空間借用 API連線
- • 平板借用系統即時查詢
- • 生生用平板車/充電車借還規程
- • 電腦教室與卓越基地預約規範
校園資安與個資
- • 資安紅線(嚴禁私設 AP 熱點)
- • 社交工程釣魚郵件演練防範
- • 公私信箱分流與密碼更換原則
教室設備簡易排障
- • 投影機/大屏無畫面聲音排查 3 步
- • 教室實體網路孔無法上網排查
- • 筆電投影快捷鍵 Win+P 指引
設備報修與耗材
- • 校園線上設備報修系統流程
- • 緊急故障即時電話通報專線
- • 公務耗材領用與登記流程
小學生都能跟著做!安裝 6 大步驟
跟著以下順序做,每完成一步就打勾,10 分鐘內就能上線!
建立 LINE 機器人,取得 Access Token
約 2 分鐘完成
- 進入 LINE Developers 後台 ,點右上角 Log in 使用您平時的 LINE 帳號登入。
- 點擊 Create a new provider,輸入您的組織名稱(例如:
[學校名稱] 資訊組)。 - 在 Provider 內點選 Create a Messaging API channel:
- Channel name:輸入機器人名稱(例如:校園資訊組 AI 小幫手)
- Channel description:輸入簡介(例如:協助全校同仁解決各項校園資訊、網路、研習與平板借用查詢)
- Category:選擇「學校、教育」相關分類,並勾選同意條款後按下 Create。
- 建立後點進該 Channel,切換到上方 Messaging API 頁籤:
- 拉到頁面最下方找到 Channel access token,點擊 Issue 生成金鑰,並將這串很長的英文數字複製備用!
- 在同頁面找到 Auto-reply messages,點進去將「自動回應訊息」改為 關閉、「Webhook」改為 啟用。
取得免費 Google Gemini API Key
約 1 分鐘完成
- 前往 Google AI Studio 官網 ,登入您的 Google 帳號。
- 點擊左上角或右上角的 Get API key(取得 API 金鑰)。
- 點選 Create API key,選擇預設的 Google Cloud 專案並點擊建立。
- 複製生成的以
AIzaSy...開頭的金鑰代碼,稍後填入後端即可(Google 提供超大額度的免費呼叫量,完全免綁信用卡)。
建立 Google 試算表(知識庫大腦)
約 1 分鐘完成
- 打開 Google 試算表 (sheets.new) ,建立一份全新空白試算表。
- 將試算表標題命名為:
校園資訊組智慧小幫手知識庫。 - 下方預設的工作表「工作表1」可以不用手動改名或手動貼資料,等一下貼好程式碼後執行一次「初始化函式」,它會自動幫您建立「資訊組知識庫」與「對話紀錄」並填入 8 大核心領域完整範例!
貼上 Apps Script 程式碼(Code.gs & index.html)
約 2 分鐘完成
- 在剛才建立的 Google 試算表上方選單,點選 擴充功能 ➔ Apps Script。
- 進入編輯器後,將右側原本預設的
myFunction全部刪除清空。 - 點擊下方【一鍵複製 Code.gs】,貼入 程式碼.gs (Code.gs) 編輯區,並在最上方的
CONFIG中貼入您的 GEMINI_API_KEY 與 LINE_ACCESS_TOKEN。 - 點擊左側檔案列表上方的 「+」號 ➔ 選擇「HTML」,將檔案名稱命名為
index(請直接輸入 index,系統會自動補上 .html)。 - 點擊下方【一鍵複製 index.html】,將內容貼進 index.html 檔案中。
- 點擊上方 磁碟片圖示(儲存專案)。
發布部署 & 回填 LINE Webhook 網址
約 2 分鐘完成
- 在 Apps Script 編輯器右上角,點擊藍色 部署 (Deploy) ➔ 新增部署作業 (New deployment)。
- 點擊左側齒輪圖示 ➔ 選擇 網頁應用程式 (Web App):
- 說明:輸入
v1 - 執行身分:選擇 我 (自己的 Google 帳號)
- 誰可以存取 (Who has access):務必選擇「所有人 (Anyone)」(這樣 LINE 伺服器才能把訊息傳進來!)。
- 說明:輸入
- 按下 部署,若跳出授權視窗,點擊「核准存取權」➔「進階 (Advanced)」➔「前往專案(不安全)」➔「允許」。
- 部署完成後,複製畫面上顯示的 網頁應用程式網址 (Web App URL)。
- 回到 LINE Developers ➔ Messaging API 頁籤:
- 找到 Webhook URL 欄位,貼上剛剛複製的網址並儲存。
- 點擊 Verify(驗證) 按鈕,看到綠色 Success 即表示連線成功!
- 確認 Use webhook 開關已切換為 開啟 (ON)。
一鍵初始化資料庫 & 手機測試!
最後一步!即刻上線
- 回到 Apps Script 編輯器上方,將函式下拉選單切換為
initKnowledgeBaseSheet,點擊 執行 (Run)。 - 執行完畢後切回 Google 試算表,您會發現試算表已經自動填滿 8 大核心領域的完整問答範例!
- 拿出手機開啟 LINE,掃描 LINE Developers 頁面上的 QR code 加入您自己的小幫手好友。
- 傳送一則訊息:「請問這節課是誰借平板車?」或「校園 Wi-Fi 怎麼設定?」
- 🎉 恭喜!AI 智慧小幫手已經在 1 秒內連線資料庫親切又準確地回覆您囉!
專案核心檔案・一鍵複製打包中心
點擊下方右上角的「複製」按鈕,直接帶走完整、可立即運行的通用原始程式碼。
/**
* 竹光國中教務處暨資訊組 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 + ' 筆資料!');
}
<!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 行金鑰是否填妥並已按儲存部署。
可以把小幫手拉進學校全體教職員大群組嗎?
完全可以! 程式碼已內建群組防洗版過濾機制:在群組中,只有老師主動 @小幫手 或提及「資訊組長/小幫手」時它才會發言,平常絕不干擾群組對話。