--- name: quiz-builder description: LINE Bot 題庫系統的題目建立流程,包含 JSON 格式規範、命名規則、圖片規範。當需要建立新題庫、維護現有題目、或查詢題庫格式時使用此 skill。 --- # LINE Bot 題庫建立指南 ## 專案資訊(2026-01-19 更新) ### ⚠️ 重要:兩個 Bot 的區別 | Bot 名稱 | Webhook URL | config.php 位置 | |----------|-------------|-----------------| | **Dietitian Dilbert(主要)** | `https://lt4.mynet.com.tw/linebot/webhook.php` | `/linebot/config.php` | | Quiz Bot(測試用) | `https://lt4.mynet.com.tw/linebot/quiz/webhook.php` | `/linebot/quiz/config.php` | **修改題庫章節時,必須修改 `/linebot/config.php`,不是 `/linebot/quiz/config.php`!** ### 路徑對照表 | 項目 | 路徑/URL | |------|----------| | **主 Bot config** | `/home/lt4.mynet.com.tw/public_html/linebot/config.php` | | **題庫 JSON 目錄** | `/home/lt4.mynet.com.tw/public_html/linebot/quiz/` | | **圖片 URL** | `https://lt4.mynet.com.tw/linebot/images/` | | **核心程式庫** | `/home/lt4.mynet.com.tw/linebot_core/` | ## 檔案結構(2026-01-19 更新) ``` /home/lt4.mynet.com.tw/ │ ├── linebot_core/ # 共用程式庫 │ ├── LineBot.php │ ├── Analytics.php │ └── ... │ └── public_html/linebot/ │ │ # ===== 主 Bot:Dietitian Dilbert ===== ├── webhook.php # ⭐ 主 Webhook ├── config.php # ⭐ 主設定(修改章節改這裡!) ├── handlers/ │ └── MainHandler.php ├── data/ │ └── sessions.json │ │ # ===== 題庫 JSON(供主 Bot 使用)===== ├── quiz/ │ ├── chemistry/ # 普通化學(29 章節) │ │ ├── {chapter}-quiz.json │ │ └── {chapter}-answers.json │ ├── physiology/ # 人體生理學(6 章節) │ ├── nutrition/ # 營養學(2 章節) │ ├── biology/ # 普通生物學(9 章節) │ │ ├── ch1-intro-biology-quiz.json │ │ ├── ch1-1-lecture-simulation-quiz.json # 講義模擬試題 │ │ └── ... │ │ │ │ # --- 以下是獨立 Quiz Bot(測試用)--- │ ├── config.php # 另一個 Bot 的設定 │ ├── webhook.php # 另一個 Bot 的 Webhook │ └── handlers/ │ └── images/ # 共用圖片 ``` ## 命名規則 ### 檔案命名 ``` ch{章}-{節}-{英文主題}-quiz.json ch{章}-{節}-{英文主題}-answers.json ``` **範例**: - `ch2-1-classification-quiz.json` - 2.1 物質的分類 - `ch3-4-atomic-number-mass-quiz.json` - 3.4 原子序與質量數 - `ch5-3-naming-ionic-compounds-quiz.json` - 5.3 離子化合物命名 ### config.php 對應 ```php 'chapters' => [ 'ch2-1-classification' => '2.1 物質的分類', 'ch3-4-atomic-number-mass' => '3.4 原子序與質量數', ] ``` **注意**:config.php 的 key 要與檔名前綴一致(不含 `-quiz.json`) ## JSON 格式規範 ### 題目檔 (*-quiz.json) ```json { "metadata": { "title": "章節標題(中文)", "subject": "普通化學", "chapter": "2", "section": "2.1", "topic": "English Topic Name", "description": "本章節涵蓋的內容說明", "total_questions": 30, "version": "1.0", "created_date": "2026-01-06" }, "questions": [ { "id": 1, "question": "題目文字", "question_image": null, "options": { "A": "選項A", "B": "選項B", "C": "選項C", "D": "選項D" }, "options_image": null } ] } ``` ### 答案檔 (*-answers.json) ```json { "metadata": { "title": "章節標題 - 答案與解析", "subject": "普通化學", "chapter": "2", "section": "2.1", "total_questions": 30, "version": "1.0", "created_date": "2026-01-06" }, "answers": [ { "id": 1, "answer": "C", "explanation": "詳細解釋為什麼答案是 C...", "explanation_image": null } ] } ``` ## 題目設計原則 ### 每節題目數量 - **標準**:每節 30 題 - **分布**:基礎概念 10 題、應用計算 10 題、進階理解 10 題 ### 題目類型分配 | 類型 | 數量 | 說明 | |------|------|------| | 定義/概念 | 8-10 題 | 基本名詞定義 | | 判斷/比較 | 6-8 題 | 比較差異、判斷正誤 | | 計算題 | 5-8 題 | 數值計算(視章節) | | 應用題 | 4-6 題 | 生活應用、實驗情境 | | 圖表題 | 2-4 題 | 需要圖片的題目 | ### 題目撰寫要點 1. **題幹清晰**:避免歧義,一題一問 2. **選項對等**:長度相近,格式一致 3. **干擾項合理**:常見錯誤概念 4. **答案明確**:只有一個最佳答案 ## 圖片規範 ### 圖片命名 ``` ch{章}-{節}-q{題號}-{描述}.png # 題目圖片 ch{章}-{節}-a{題號}-{描述}-answer.png # 答案解析圖片 ``` **範例**: - `ch2-7-q12-heating-curve.png` - 題目圖 - `ch2-7-a12-heating-curve-answer.png` - 答案解析圖 ### 圖片 URL 格式 ``` https://lt4.mynet.com.tw/linebot/images/{檔名}.png ``` ### 需要圖片的題目類型 - 加熱/冷卻曲線圖 - 相圖 (Phase Diagram) - 週期表區域標示 - 原子/分子結構圖 - 路易士結構式 - 離子晶格結構 - 實驗裝置圖 - 數據比較圖表 ## 建立流程 ### Step 1:規劃題目 ```markdown ## 章節:2.7 狀態變化 ### 主題涵蓋 - 熔化、凝固、汽化、凝結、昇華、凝華 - 熔化熱、汽化熱 - 加熱曲線 - 相圖 ### 題目分配 - 定義題:10 題 (Q1-10) - 計算題:8 題 (Q11-18) - 應用題:8 題 (Q19-26) - 圖表題:4 題 (Q12, Q15, Q27, Q30) ``` ### Step 2:建立題目檔 使用 Write 工具建立 JSON: ```bash # 檔案路徑 C:\Users\user\linebot-quiz\quiz\chemistry\ch2-7-state-changes-quiz.json ``` ### Step 3:建立答案檔 ```bash # 檔案路徑 C:\Users\user\linebot-quiz\quiz\chemistry\ch2-7-state-changes-answers.json ``` ### Step 4:驗證 JSON ```bash cd /c/Users/user/linebot-quiz python -m json.tool quiz/chemistry/ch2-7-state-changes-quiz.json > /dev/null && echo "Quiz JSON valid" python -m json.tool quiz/chemistry/ch2-7-state-changes-answers.json > /dev/null && echo "Answers JSON valid" ``` ### Step 5:更新 config.php ```php 'ch2-7-state-changes' => '2.7 狀態變化', ``` ### Step 6:推送到 GitHub ```bash cd /c/Users/user/linebot-quiz git add . git commit -m "新增 2.7 狀態變化 (30題)" git push origin master ``` ### Step 7:部署到伺服器 ```bash # 同步題庫 scp quiz/chemistry/ch2-7-*.json lt4:/home/lt4.mynet.com.tw/public_html/linebot/quiz/chemistry/ # 更新 config.php scp config.php lt4:/home/lt4.mynet.com.tw/public_html/linebot/ ``` ## 批量建立技巧 ### 使用 TodoWrite 追蹤進度 ``` - [ ] 2.1 物質的分類 (30題) - [ ] 2.2 物質的狀態與性質 (30題) - [x] 2.3 溫度 (30題) ✓ ``` ### 平行建立多章節 同時建立題目檔和答案檔,減少來回切換: ``` 1. 規劃所有章節的題目大綱 2. 逐一建立 quiz.json 3. 逐一建立 answers.json 4. 批量驗證 5. 一次性推送 ``` ## 驗證清單 - [ ] JSON 語法正確(python -m json.tool) - [ ] 題目數量正確(30題) - [ ] id 從 1 開始連續編號 - [ ] 每題都有 4 個選項 (A/B/C/D) - [ ] 答案只有一個字母 - [ ] 圖片 URL 格式正確 - [ ] config.php 已更新 - [ ] Git 已推送 - [ ] 伺服器已同步 --- ## 題庫自動化審計(2026-01-13 新增) ### 審計腳本功能 建立 Python 腳本自動檢測題目與答案的適配問題: ```python # audit_quiz.py # 圖片關鍵字 - 題目提到這些字眼但沒圖片時發出警告 IMAGE_KEYWORDS = ['上圖', '下圖', '圖中', '看圖', '圖片', '圖表', '圖示', '觀察圖'] # 無意義選項 - 選項只有 A/B/C/D 沒有實際內容 MEANINGLESS_OPTIONS = [ {'A': 'A', 'B': 'B', 'C': 'C', 'D': 'D'}, {'A': '選項A', 'B': '選項B', 'C': '選項C', 'D': '選項D'}, ] def audit_quiz_file(quiz_path): """審計單一題庫檔案""" issues = [] with open(quiz_path, 'r', encoding='utf-8') as f: quiz_data = json.load(f) for q in quiz_data.get('questions', []): qid = q['id'] question_text = q['question'] question_image = q.get('question_image') options = q.get('options', {}) # 檢查 1:題目提到圖但沒有圖片 needs_image = any(kw in question_text for kw in IMAGE_KEYWORDS) if needs_image and not question_image: issues.append({ 'id': qid, 'type': 'missing_image', 'detail': 'Question mentions image but question_image is null' }) # 檢查 2:無意義選項 if options in MEANINGLESS_OPTIONS: issues.append({ 'id': qid, 'type': 'meaningless_options', 'detail': 'Options are just A/B/C/D with no content' }) # 檢查 3:圖片 URL 不可存取 if question_image: if not check_image_url(question_image): issues.append({ 'id': qid, 'type': 'broken_image', 'detail': 'Image URL returns non-200 status' }) return issues ``` ### 審計輸出範例 ``` [Nutrition] ch7-protein-quiz.json: 46/50 OK, 4 issues - Q9: missing_image - Q10: missing_image - Q18: meaningless_options - Q29: missing_image TOTAL: 326/330 OK (4 issues) ``` ### 問題修復方式 | 問題類型 | 修復方法 | |----------|----------| | `missing_image` | 用 matplotlib 生成圖片並上傳,更新 JSON 中的 `question_image` | | `meaningless_options` | 修改選項為有意義的內容(如「鍵結 A」「胺基酸 A」) | | `broken_image` | 檢查 URL 路徑,確認圖片已上傳至伺服器 | ### 圖片 URL 快取破壞 修復圖片後,記得加上版本參數避免 LINE 快取: ```json "question_image": "https://lt4.mynet.com.tw/linebot/images/ch7-q9-peptide-bond.png?v=1" ``` ## 常見錯誤 ### 題目文字方向性錯誤 **問題**:LINE Bot 的 Flex Message 中,圖片顯示在題目文字**上方**,但題目文字卻寫「下圖」。 ```json // 錯誤:使用「下圖」 "question": "下圖顯示某些元素符號,標示 X 的元素是?" // 正確:使用「上圖」 "question": "上圖顯示某些元素符號,標示 X 的元素是?" ``` **批量修正**: ```bash ssh lt4 "cd /home/lt4.mynet.com.tw/public_html/linebot/quiz/chemistry && sed -i 's/下圖/上圖/g' *.json" ``` ### 「標示 X」題目的圖片缺少 X 標記 **問題**:題目問「標示 X 的是什麼?」,但圖片中所有內容都完整顯示,沒有任何 X 標記。 **正確做法**:圖片中必須用 **X** 遮蓋答案,讓學生猜測。 | 題目類型 | 圖片應該顯示 | |----------|-------------| | 「標示 X 的元素是?」答案:銅(Cu) | 元素表中 Cu 的位置顯示紅色 **X** | | 「標示 X 的區域是?」答案:過渡金屬 | 週期表中過渡金屬區域顯示 **X** | | 「標示 X 的部分是?」答案:原子核 | 原子結構圖中原子核位置顯示 **X** | **Python 範例**(用 PIL 加入 X 標記): ```python from PIL import Image, ImageDraw, ImageFont def add_x_mark(draw, x, y, font_size=72, color='red'): """在指定位置加入 X 標記""" font = ImageFont.truetype("C:/Windows/Fonts/msjh.ttc", font_size) draw.text((x, y), "X", font=font, fill=color, anchor='mm') ``` ### 「標示 X」圖片必須有邏輯可循 **問題**:圖片中的元素隨意排列,即使有 X 標記,學生也無法從規律推斷答案,等於盲猜。 **錯誤示範**: - 隨便放 12 個元素(H, C, N, O, Na, Mg...),把其中一個改成 X - 學生無法從排列規律判斷 X 是什麼 **正確做法**:圖片排列必須有邏輯,讓學生可以根據規律推斷答案。 | 題目 | 正確設計 | |------|----------| | 「原子序 29,標示 X 的元素是?」 | 按週期表順序排列:K(19)→Ca(20)→...→Ni(28)→**X**(29)→Zn(30) | | 「3-12族,標示 X 的區域是?」 | 顯示 s區(1-2族)、**X**(3-12族)、p區(13-18族) | **範例:有邏輯的元素符號圖** ```python # 按週期表第四週期順序排列,顯示原子序 elements = [ ('K', '鉀', '19'), ('Ca', '鈣', '20'), ('Fe', '鐵', '26'), ('Co', '鈷', '27'), ('Ni', '鎳', '28'), ('X', '?', '29'), # Cu 遮蓋成 X ('Zn', '鋅', '30'), ] # 學生看到原子序 29,可推斷是 Cu(銅) ``` **字體大小建議(PIL/Pillow)**: ```python title_font = get_font(60) # 標題 element_font = get_font(72) # 元素符號/X 標記 number_font = get_font(36) # 原子序 chinese_font = get_font(40) # 中文名稱 question_font = get_font(44) # 題目文字 ``` ### 圖片與題目不匹配的排查 **檢查清單**: 1. 圖片中是否有題目描述的標記(X、箭頭、問號等) 2. 圖片中標記的位置是否對應正確答案 3. 題目文字的方向描述(上圖/下圖)是否正確 **排查指令**: ```bash # 找出所有「標示 X」的題目 ssh lt4 "grep -rn '標示.*X' /home/lt4.mynet.com.tw/public_html/linebot/quiz/*/*.json" # 列出這些題目對應的圖片 URL ssh lt4 "grep -B1 '標示.*X' /home/lt4.mynet.com.tw/public_html/linebot/quiz/*/*.json | grep question_image" ``` ### JSON 語法錯誤 ```json // 錯誤:最後一項有逗號 {"id": 30, "answer": "C", "explanation": "..."}, ] // 正確:最後一項無逗號 {"id": 30, "answer": "C", "explanation": "..."} ] ``` ### 選項格式錯誤 ```json // 錯誤:選項是陣列 "options": ["A選項", "B選項", "C選項", "D選項"] // 正確:選項是物件 "options": {"A": "選項A", "B": "選項B", "C": "選項C", "D": "選項D"} ``` ### 圖片路徑錯誤 ```json // 錯誤:相對路徑 "question_image": "images/ch2-7-q12.png" // 正確:完整 URL "question_image": "https://lt4.mynet.com.tw/linebot/images/ch2-7-q12-heating-curve.png" ``` ## 範本 ### 快速建立範本 複製此範本開始新章節: ```json { "metadata": { "title": "【填入中文標題】", "subject": "普通化學", "chapter": "【章】", "section": "【章.節】", "topic": "【English Topic】", "description": "【描述】", "total_questions": 30, "version": "1.0", "created_date": "【YYYY-MM-DD】" }, "questions": [ {"id": 1, "question": "", "question_image": null, "options": {"A": "", "B": "", "C": "", "D": ""}, "options_image": null} ] } ``` --- ## 圖片生成指南(手機可讀性) ### 重要:LINE Bot 圖片字體大小 LINE Bot 在手機上顯示圖片時,使用者**無法放大**圖片。因此圖片中的文字必須足夠大才能閱讀。 ### 建議字體大小 | 用途 | 字體大小 | 說明 | |------|----------|------| | 標題 | **48pt** | 圖片主標題 | | 標籤 | **36pt** | 重要元素標籤 | | 說明文字 | **32pt** | 一般解說文字 | | 小字 | **28pt** | 次要資訊(最小不要低於此) | > **注意**:原本建議的 36/28/24/20pt 在 LINE Bot 手機上仍可能太小,建議使用上述更大的字體。 ### Python 圖片生成範本 使用 matplotlib 生成教育圖片: ```python # -*- coding: utf-8 -*- import matplotlib.pyplot as plt from matplotlib.patches import FancyBboxPatch, Circle, Ellipse import os # 中文字體設定 plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei', 'SimHei', 'Arial Unicode MS'] plt.rcParams['axes.unicode_minus'] = False # 字體大小常數(適合手機閱讀) FONT_TITLE = 36 FONT_LABEL = 28 FONT_TEXT = 24 FONT_SMALL = 20 # 輸出目錄 OUTPUT_DIR = r"C:\Users\user\Documents\temp\images" os.makedirs(OUTPUT_DIR, exist_ok=True) def save_fig(fig, filename): """儲存圖片 - 150 DPI 足夠清晰且檔案不會太大""" filepath = os.path.join(OUTPUT_DIR, filename) fig.savefig(filepath, dpi=150, bbox_inches='tight', facecolor='white', edgecolor='none') plt.close(fig) print(f"已儲存: {filename}") def create_example_diagram(): """範例圖片生成函數""" fig, ax = plt.subplots(figsize=(14, 10)) # 14x10 英寸 ax.set_xlim(0, 14) ax.set_ylim(0, 10) ax.axis('off') # 標題 ax.set_title('圖片標題', fontsize=FONT_TITLE, fontweight='bold', pad=20) # 繪製內容... box = FancyBboxPatch((2, 3), 4, 3, boxstyle="round,pad=0.1", facecolor='#BBDEFB', edgecolor='#1565C0', linewidth=2) ax.add_patch(box) ax.text(4, 4.5, '標籤文字', ha='center', fontsize=FONT_LABEL, fontweight='bold') ax.text(4, 3.5, '說明文字', ha='center', fontsize=FONT_TEXT) save_fig(fig, 'example-diagram.png') ``` ### 圖片生成腳本組織 建議為每個章節建立獨立的 Python 腳本: ``` C:\Users\user\Documents\temp\ ├── generate_physiology_ch1_large.py ├── generate_physiology_ch2_large.py ├── generate_physiology_ch7_large.py ├── generate_physiology_ch8_large.py ├── generate_physiology_ch17_large.py └── images/ ├── ch1-a3-organization-levels.png ├── ch1-a9-negative-feedback.png └── ... ``` ### 圖片命名規則 **人體生理學**(無小節): ``` ch{章}-a{題號}-{英文描述}.png ``` 範例:`ch8-a9-neuron-structure.png` **普通化學**(有小節): ``` ch{章}-{節}-a{題號}-{英文描述}.png ``` 範例:`ch2-7-a12-heating-curve-answer.png` ### 常用 matplotlib 元件 ```python from matplotlib.patches import ( FancyBboxPatch, # 圓角方框 Circle, # 圓形 Ellipse, # 橢圓 Polygon, # 多邊形 Rectangle, # 矩形 ) # 圓角方框 box = FancyBboxPatch((x, y), width, height, boxstyle="round,pad=0.1", facecolor='#BBDEFB', edgecolor='#1565C0', linewidth=2) # 箭頭 ax.annotate('', xy=(end_x, end_y), xytext=(start_x, start_y), arrowprops=dict(arrowstyle='->', color='#424242', lw=2)) # 雙向箭頭 ax.annotate('', xy=(x2, y), xytext=(x1, y), arrowprops=dict(arrowstyle='<->', color='#424242', lw=2)) ``` ### 配色建議 使用 Material Design 色彩,易於辨識: | 顏色 | 填充色 | 邊框色 | 用途 | |------|--------|--------|------| | 藍色 | #BBDEFB | #1565C0 | 一般元素 | | 綠色 | #C8E6C9 | #2E7D32 | 正確/正面 | | 紅色 | #FFCDD2 | #C62828 | 警告/重點 | | 橘色 | #FFE0B2 | #E65100 | 次要元素 | | 紫色 | #E1BEE7 | #7B1FA2 | 特殊標記 | | 灰色 | #ECEFF1 | #607D8B | 背景/中性 | --- ## 多科目支援 ### 目前支援科目 ```php $SUBJECTS = [ 'chemistry' => [ 'name' => '普通化學', 'chapters' => [...] ], 'physiology' => [ 'name' => '人體生理學', 'chapters' => [...] ], 'nutrition' => [ 'name' => '營養學', 'chapters' => [ 'ch6-lipids' => '第六章 脂質', 'ch7-protein' => '第七章 蛋白質', ] ], ]; ``` ### 檔案結構(多科目) ``` linebot-quiz/ ├── config.php ├── quiz/ │ ├── chemistry/ │ │ ├── ch2-1-classification-quiz.json │ │ └── ch2-1-classification-answers.json │ └── physiology/ │ ├── ch1-introduction-quiz.json │ └── ch1-introduction-answers.json └── images/ ├── ch2-1-q30-classification.png # 化學 └── ch8-a9-neuron-structure.png # 人體生理學 ``` ### 新增科目步驟 1. 在 `quiz/` 下建立科目目錄 2. 在 `config.php` 的 `$SUBJECTS` 新增科目設定 3. 建立題目和答案 JSON 檔案 4. 生成所需圖片並上傳 --- ## 部署注意事項 ### SSH 連線設定 確保 `~/.ssh/config` 有正確設定: ``` Host lt4 HostName 172.104.67.123 User root IdentityFile ~/.ssh/id_ed25519 IdentitiesOnly yes ``` ### 批量上傳圖片 ```bash # 上傳特定章節圖片 scp "C:/Users/user/Documents/temp/images/ch8-a"*.png lt4:/home/lt4.mynet.com.tw/public_html/linebot/images/ # 上傳所有人體生理學圖片 scp "C:/Users/user/Documents/temp/images/ch1-a"*.png \ "C:/Users/user/Documents/temp/images/ch2-a"*.png \ "C:/Users/user/Documents/temp/images/ch7-a"*.png \ "C:/Users/user/Documents/temp/images/ch8-a"*.png \ "C:/Users/user/Documents/temp/images/ch17-a"*.png \ lt4:/home/lt4.mynet.com.tw/public_html/linebot/images/ ``` ### 驗證上傳 ```bash ssh lt4 "ls /home/lt4.mynet.com.tw/public_html/linebot/images/ch*-a*.png | wc -l" ``` --- ## LINE 圖片快取問題與解決方案 ### 問題描述 LINE 會積極快取圖片。當你更新伺服器上的圖片後,LINE 可能仍顯示舊版本(甚至空白圖片),因為 URL 沒變。 ### 解決方案:快取破壞參數 在圖片 URL 後加入版本參數,強制 LINE 重新載入: ```json // 更新前 "explanation_image": "https://lt4.mynet.com.tw/linebot/images/ch2-2-a15-states-answer.png" // 更新後(加入 ?v=2) "explanation_image": "https://lt4.mynet.com.tw/linebot/images/ch2-2-a15-states-answer.png?v=2" ``` ### 批量更新快取破壞參數 ```bash # 更新所有化學答案檔 ssh lt4 "cd /home/lt4.mynet.com.tw/public_html/linebot/quiz/chemistry && sed -i 's/\.png\"/\.png?v=2\"/g' *-answers.json" # 更新所有人體生理學答案檔 ssh lt4 "cd /home/lt4.mynet.com.tw/public_html/linebot/quiz/physiology && sed -i 's/\.png\"/\.png?v=2\"/g' *-answers.json" ``` --- ## LINE Flex Message 圖片顯示優化 ### 圖片比例設定 webhook.php 中的 `aspectRatio` 設定影響圖片顯示大小: | 比例 | 效果 | 適用場景 | |------|------|----------| | 16:9 | 寬扁,文字較小 | 橫向圖表 | | **4:3** | 較高,文字較大 | **教育圖片(推薦)** | | 1:1 | 正方形 | 圖標類 | ### 修改方式 ```php // webhook.php 中的 hero 設定 $flexContents['hero'] = [ 'type' => 'image', 'url' => $imageUrl, 'size' => 'full', 'aspectRatio' => '4:3', // 改為 4:3 讓圖片更高 'aspectMode' => 'fit' ]; ``` --- ## Python 圖片生成進階技巧 ### 超大字體設定(強烈建議) 原本的 36/28/24/20pt 在手機上仍可能太小。建議使用 **48/36/32/28pt**: ```python # 超大字體設定 (LINE Bot 手機閱讀優化) FONT_TITLE = 48 # 標題 FONT_LABEL = 36 # 標籤 FONT_TEXT = 32 # 內文 FONT_SMALL = 28 # 小字(最小不要低於此) ``` ### save_fig 函數陷阱 **問題**:連續呼叫 `save_fig` 兩次會導致第二個檔案為空白! ```python # 錯誤示範 save_fig('ch2-2-q15-states.png') # 正常儲存 save_fig('ch2-2-a15-states-answer.png') # 空白!因為 figure 已關閉 def save_fig(filename): plt.savefig(...) plt.close() # 這行關閉了 figure ``` **正確做法**:修改 save_fig 同時儲存 q 和 a 版本: ```python def save_fig(fig, filename): """儲存圖片 - 同時儲存 q 和 a 兩種版本""" # 儲存 a 版本 (原始) filepath_a = os.path.join(OUTPUT_DIR, filename) fig.savefig(filepath_a, dpi=150, bbox_inches='tight', facecolor='white', edgecolor='none') print(f"已儲存: {filename}") # 儲存 q 版本 (將 -a 改為 -q) if '-a' in filename: filename_q = filename.replace('-a', '-q', 1) filepath_q = os.path.join(OUTPUT_DIR, filename_q) fig.savefig(filepath_q, dpi=150, bbox_inches='tight', facecolor='white', edgecolor='none') print(f"已儲存: {filename_q}") plt.close(fig) ``` ### 完整範例腳本結構 ```python # -*- coding: utf-8 -*- import matplotlib.pyplot as plt from matplotlib.patches import FancyBboxPatch, Circle import os plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei', 'SimHei'] plt.rcParams['axes.unicode_minus'] = False OUTPUT_DIR = r"C:\Users\user\Documents\temp\images" os.makedirs(OUTPUT_DIR, exist_ok=True) # 超大字體 FONT_TITLE = 48 FONT_LABEL = 36 FONT_TEXT = 32 FONT_SMALL = 28 def save_fig(fig, filename): """儲存圖片 - 同時儲存 q 和 a 兩種版本""" filepath_a = os.path.join(OUTPUT_DIR, filename) fig.savefig(filepath_a, dpi=150, bbox_inches='tight', facecolor='white', edgecolor='none') print(f"已儲存: {filename}") if '-a' in filename: filename_q = filename.replace('-a', '-q', 1) filepath_q = os.path.join(OUTPUT_DIR, filename_q) fig.savefig(filepath_q, dpi=150, bbox_inches='tight', facecolor='white', edgecolor='none') print(f"已儲存: {filename_q}") plt.close(fig) def create_example(): fig, ax = plt.subplots(figsize=(14, 10)) ax.set_xlim(0, 14) ax.set_ylim(0, 10) ax.axis('off') ax.set_title('範例圖', fontsize=FONT_TITLE, fontweight='bold', pad=20) # ... 繪製內容 ... save_fig(fig, 'ch1-a1-example.png') # 會同時生成 q 和 a 版本 if __name__ == '__main__': create_example() ``` --- ## 故障排除 ### 答案圖片不顯示 1. **檢查 URL 是否正確**: ```bash ssh lt4 "grep 'explanation_image' /path/to/answers.json | head -3" ``` 2. **檢查圖片是否存在**: ```bash ssh lt4 "curl -I https://lt4.mynet.com.tw/linebot/images/ch2-2-a15-states-answer.png" ``` 3. **檢查圖片是否為空白**(檔案很小可能是空白): ```bash ssh lt4 "ls -la /path/to/image.png" # 小於 10KB 可能有問題 ssh lt4 "file /path/to/image.png" # 確認是有效 PNG ``` 4. **加入快取破壞參數**: ```bash ssh lt4 "sed -i 's/\.png\"/\.png?v=2\"/g' /path/to/answers.json" ``` ### 圖片文字太小 1. 增加字體大小(至少 FONT_SMALL = 28) 2. 修改 webhook.php 的 aspectRatio 為 4:3 3. 重新生成圖片並上傳 4. 更新快取破壞參數 --- ## 伺服器部署安全須知(重要!) ### config.php 敏感資訊保護 **問題**:本地的 `config.php` 包含佔位符,伺服器上的 `config.php` 包含真實的 LINE 憑證。使用 `scp` 同步時會**覆蓋伺服器上的真實憑證**,導致 LINE Bot 完全失效。 **症狀**: - LINE Bot 完全沒有反應 - 輸入任何文字都沒有回應 - debug.log 顯示 `Invalid signature` **解決方案**: 1. **永遠不要直接同步 config.php 到伺服器**: ```bash # 危險!不要這樣做 scp config.php lt4:/home/lt4.mynet.com.tw/public_html/linebot/ # 安全做法:只同步題庫和 webhook scp webhook.php lt4:/home/lt4.mynet.com.tw/public_html/linebot/ scp quiz/chemistry/*.json lt4:/home/lt4.mynet.com.tw/public_html/linebot/quiz/chemistry/ ``` 2. **如果需要更新 config.php 的章節設定**: ```bash # 只更新章節設定,保留憑證 ssh lt4 "vim /home/lt4.mynet.com.tw/public_html/linebot/config.php" # 或者用 sed 只修改特定行 ssh lt4 "sed -i \"/chapters/,/]/c\\NEW_CONTENT\" /path/to/config.php" ``` 3. **備份伺服器 config.php**: ```bash ssh lt4 "cp /home/lt4.mynet.com.tw/public_html/linebot/config.php /home/lt4.mynet.com.tw/public_html/linebot/config.php.bak" ``` ### LINE 憑證位置 如果憑證被覆蓋,需要到 LINE Developers Console 重新取得: - **網址**:https://developers.line.biz/console/ - **Channel access token**:Messaging API → Channel access token (long-lived) - **Channel secret**:Basic settings → Channel secret ```php // 伺服器上的 config.php 應該包含真實憑證 define('LINE_CHANNEL_ACCESS_TOKEN', '實際的token...'); define('LINE_CHANNEL_SECRET', '實際的secret'); ``` --- ## LINE Bot 調試技巧 ### 添加調試日誌 當 LINE Bot 沒有反應時,在 `webhook.php` 開頭添加日誌功能: ```php /home/lt4.mynet.com.tw/public_html/linebot/debug.log" ``` --- ## LINE Flex Message 注意事項 ### Button vs Box 的 Action **Button 組件**(推薦): - 穩定可靠 - label 有 20 字元限制 - 適合短文字按鈕 ```php [ 'type' => 'button', 'style' => 'primary', 'height' => 'sm', 'action' => [ 'type' => 'message', 'label' => '[1] 章節名稱', // 最多 20 字元 'text' => '1' ] ] ``` **Box 組件的 Action**: - 可以包含更長的文字(使用 wrap: true) - 某些情況下 action 可能不被觸發 - 需要測試確認 ```php [ 'type' => 'box', 'layout' => 'horizontal', 'contents' => [...], 'action' => [ 'type' => 'message', 'text' => '1' ] ] ``` ### 長章節名稱處理 如果章節名稱超過 button label 限制: ```php // 截短名稱 $shortName = mb_strlen($name) > 12 ? mb_substr($name, 0, 12) . '..' : $name; $buttons[] = [ 'type' => 'button', 'action' => [ 'type' => 'message', 'label' => "[{$i}] {$shortName}", 'text' => (string)$i ] ]; ``` --- ## 部署檢查清單 ### 安全部署步驟 ```bash # 1. 備份伺服器設定 ssh lt4 "cp /home/lt4.mynet.com.tw/public_html/linebot/config.php /home/lt4.mynet.com.tw/public_html/linebot/config.php.bak.$(date +%Y%m%d%H%M)" # 2. 同步 webhook.php(先檢查語法) scp webhook.php lt4:/home/lt4.mynet.com.tw/public_html/linebot/ ssh lt4 "php -l /home/lt4.mynet.com.tw/public_html/linebot/webhook.php" # 3. 同步題庫檔案 scp quiz/chemistry/*.json lt4:/home/lt4.mynet.com.tw/public_html/linebot/quiz/chemistry/ # 4. 手動更新 config.php 章節設定(如需要) ssh lt4 "vim /home/lt4.mynet.com.tw/public_html/linebot/config.php" # 5. 測試 LINE Bot # 在 LINE 輸入「開始」確認正常運作 ``` ### 緊急恢復 如果 LINE Bot 壞掉: ```bash # 恢復 config.php 備份 ssh lt4 "cp /home/lt4.mynet.com.tw/public_html/linebot/config.php.bak /home/lt4.mynet.com.tw/public_html/linebot/config.php" # 恢復 webhook.php 備份 ssh lt4 "cp /home/lt4.mynet.com.tw/public_html/linebot/webhook.php.bak /home/lt4.mynet.com.tw/public_html/linebot/webhook.php" # 檢查日誌找出問題 ssh lt4 "tail -50 /home/lt4.mynet.com.tw/public_html/linebot/debug.log" ``` --- ## 伺服器修改最佳實踐(重要!) ### 每次修改後必做檢查 ```bash # 1. 檢查 PHP 語法 ssh lt4 "php -l /home/lt4.mynet.com.tw/public_html/linebot/config.php" ssh lt4 "php -l /home/lt4.mynet.com.tw/public_html/linebot/webhook.php" # 2. 檢查 HTTP 狀態(應該是 400,不是 500) curl -s -o /dev/null -w '%{http_code}' https://lt4.mynet.com.tw/linebot/webhook.php # 400 = 正常(缺少 LINE 簽名) # 500 = PHP 錯誤! # 3. 清除 session 讓用戶重新開始 ssh lt4 "echo '{}' > /home/lt4.mynet.com.tw/public_html/linebot/data/sessions.json" ``` ### 不要用 sed 修改 PHP 陣列! **危險操作**(容易產生語法錯誤): ```bash # 這樣刪除會留下多餘的括號! ssh lt4 "sed -i \"/'physiology' => \[/,/\]/d\" config.php" ``` **問題**:sed 刪除多行 PHP 陣列時,容易留下多餘的 `]` 或 `,` 導致語法錯誤。 **安全做法**: 1. **用 vim 直接編輯**: ```bash ssh lt4 "vim /home/lt4.mynet.com.tw/public_html/linebot/config.php" ``` 2. **用 PHP 腳本修改**: ```bash ssh lt4 "php -r \" \\\$config = file_get_contents('config.php'); // 做修改... file_put_contents('config.php', \\\$config); \"" ``` 3. **完整重寫該段落**(推薦): ```bash # 先備份 ssh lt4 "cp config.php config.php.bak" # 用 heredoc 重寫整個 $SUBJECTS 陣列 ``` ### LINE Bot 完全沒反應的排查流程 ``` 1. 檢查 HTTP 狀態 curl -s -o /dev/null -w '%{http_code}' https://lt4.mynet.com.tw/linebot/webhook.php ├─ 500 → PHP 錯誤 │ → php -l config.php │ → php -l webhook.php │ ├─ 400 → 正常,檢查日誌 │ → tail debug.log │ ├─ "Invalid signature" → 憑證錯誤 │ └─ 空的 → LINE webhook URL 設定錯誤 │ └─ 其他 → 伺服器/網路問題 ``` ### 修改 config.php 科目設定的安全流程 ```bash # 1. 備份 ssh lt4 "cp /home/lt4.mynet.com.tw/public_html/linebot/config.php /home/lt4.mynet.com.tw/public_html/linebot/config.php.bak.$(date +%Y%m%d%H%M)" # 2. 用 sed 做簡單的文字替換(安全) ssh lt4 "sed -i \"s/'舊名稱'/'新名稱'/\" /path/to/config.php" # 3. 驗證語法 ssh lt4 "php -l /path/to/config.php" # 4. 測試 HTTP 狀態 curl -s -o /dev/null -w '%{http_code}' https://lt4.mynet.com.tw/linebot/webhook.php # 5. 清除 session ssh lt4 "echo '{}' > /path/to/sessions.json" # 6. 在 LINE 輸入「開始」測試 ``` --- ## LINE Flex Message 按鈕折行解法(2026-01-08 新增) ### 問題:Button label 字元限制 LINE Flex Message 的 `type: button` 元件,label 有約 **20 字元限制**,超過會被截斷,無法顯示完整選項。 ### 解法:改用 Box + Text (wrap: true) ```php // 原本的 button(會截斷) $optionButtons[] = [ 'type' => 'button', 'style' => 'primary', 'action' => [ 'type' => 'message', 'label' => "({$key}) {$value}", // 超過 20 字元會截斷! 'text' => $key ] ]; // 改用 box + text(支援折行) $optionButtons[] = [ 'type' => 'box', 'layout' => 'vertical', 'contents' => [ [ 'type' => 'text', 'text' => "({$key}) {$value}", 'wrap' => true, // 關鍵:啟用折行 'color' => '#ffffff', 'size' => 'sm', 'align' => 'center' ] ], 'backgroundColor' => '#5B8DEF', 'cornerRadius' => 'md', 'paddingAll' => '12px', 'margin' => 'sm', 'action' => [ 'type' => 'message', 'label' => $key, 'text' => $key ] ]; ``` --- ## 圖片 URL 串接邏輯(避免雙重路徑) ### 問題 JSON 中已存完整 URL,但 PHP 又加上 `IMAGE_BASE_URL`,導致: ``` https://lt4.mynet.com.tw/linebot/images/https://lt4.mynet.com.tw/linebot/images/ch2-7-q12.png ``` ### 解法:先檢查是否已有 http 開頭 ```php // question_image $imageUrl = (strpos($q['question_image'], 'http') === 0) ? $q['question_image'] : IMAGE_BASE_URL . '/' . $q['question_image']; // explanation_image $explanationUrl = (strpos($explanationImage, 'http') === 0) ? $explanationImage : IMAGE_BASE_URL . '/' . $explanationImage; ``` --- ## matplotlib 圖片生成標準(手機 LINE Bot 可讀) ### 字體大小標準 | 元素 | 字體大小 | 說明 | |------|----------|------| | 標籤 (X, A-E) | **44-50pt** | 粗體 + 黃底紅字,必須醒目 | | 圖片標題 | **28pt** | 粗體 | | 軸標題 | **24pt** | 粗體 | | 一般文字 | **18-20pt** | — | ### 標準設定 ```python import matplotlib.pyplot as plt # 中文字體 plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei', 'SimHei'] plt.rcParams['axes.unicode_minus'] = False # 儲存設定 fig.savefig(path, dpi=150, bbox_inches='tight', facecolor='white') ``` ### LINE Bot 最佳圖片尺寸(2026-01-13 更新) 使用 **figsize=(10.4, 7.8) + dpi=100** 可以得到精確的 **1040×780 像素**輸出,這是 LINE Bot 最佳顯示尺寸: ```python # LINE Bot 優化尺寸 FIG_W, FIG_H = 10.4, 7.8 # 英寸 DPI = 100 # 字體大小(配合此尺寸) FONT_TITLE = 42 # 標題(最大) FONT_LARGE = 32 # 大標籤 FONT_MEDIUM = 26 # 中等文字 FONT_SMALL = 22 # 小字(最小建議) def create_image(): fig, ax = plt.subplots(figsize=(FIG_W, FIG_H)) ax.set_xlim(0, 10.4) # X 範圍對應寬度 ax.set_ylim(0, 7.8) # Y 範圍對應高度 ax.axis('off') # 繪製內容... fig.savefig(filepath, dpi=DPI, bbox_inches='tight', facecolor='white', edgecolor='none', pad_inches=0.1) ``` **為何選擇這個尺寸?** - 1040×780 = 4:3 比例,LINE Bot 顯示時圖片夠大 - 100 DPI 讓座標計算直覺(1 單位 = 100 像素) - 檔案大小適中(通常 30-80 KB) ### 重點:圖片標記必須與題目一致 題目問「區域 B 代表什麼?」→ 圖片中**必須有 B 標記** ```python # 標籤範例:大字體 + 醒目背景 ax.text(x, y, 'B', fontsize=44, fontweight='bold', ha='center', va='center', color='red', bbox=dict(boxstyle='circle,pad=0.3', facecolor='yellow', edgecolor='red', linewidth=2)) ``` --- ## Git 推送衝突處理 當 `git push` 被拒絕(remote 有新變更): ```bash # 一行搞定:暫存 → 拉取 → 恢復 → 推送 git stash && git pull --rebase && git stash pop && git push ``` --- ## 題目圖片洩題檢測與修復(2026-01-11 新增) ### 問題描述 題目圖片(Q版本)不應該顯示答案,應該用「?」隱藏答案,讓學生思考。如果 Q 和 A 圖片相同,等於直接洩漏答案。 ### 洩題檢測方法 **關鍵洞見**:如果 Q 和 A 圖片的檔案大小完全相同,代表它們是同一張圖片(洩題)。 ```bash # 檢測所有 Q/A 檔案大小相同的圖片 ssh lt4 'cd /home/lt4.mynet.com.tw/public_html/linebot/images && \ for qfile in ch*-q*.png; do afile=$(echo "$qfile" | sed "s/-q/-a/") if [ -f "$afile" ]; then qsize=$(stat -c%s "$qfile") asize=$(stat -c%s "$afile") if [ "$qsize" = "$asize" ]; then echo "SAME SIZE: $qfile ($qsize bytes) = $afile" fi fi done' ``` ### Q/A 圖片設計原則 | 版本 | 目的 | 設計方式 | |------|------|----------| | **Q 版本** | 題目圖(隱藏答案) | 答案處顯示「?」 | | **A 版本** | 解答圖(顯示答案) | 完整顯示所有資訊 | ### 修復範例 **負回饋調控圖**(ch1-q9 vs ch1-a9): ```python # Q版本 - 隱藏答案 def create_q9_negative_feedback_Q(): ax.add_patch(FancyBboxPatch(...)) ax.text(x, y, '?', fontsize=FONT_TITLE, color='#C62828') # 用問號隱藏 ax.text(7, 1.5, '哪種生理調節是負回饋的例子?', style='italic') # A版本 - 顯示答案 def create_a9_negative_feedback_A(): ax.add_patch(FancyBboxPatch(...)) ax.text(x, y, '血壓調節', fontweight='bold') # 顯示答案 ax.text(7, 0.5, '答案:血壓調節是負回饋的典型例子', bbox=dict(boxstyle='round', facecolor='#E8F5E9', edgecolor='#4CAF50')) ``` ### 圖片命名規則 ``` ch{章}-q{題號}-{描述}.png # 題目版(隱藏答案) ch{章}-a{題號}-{描述}.png # 解答版(顯示答案) ``` **範例**: - `ch1-q9-negative-feedback.png` - 題目版 - `ch1-a9-negative-feedback.png` - 解答版 ### 完整修復流程 1. **檢測洩題**:比較 Q/A 檔案大小 2. **分析題目**:讀取 JSON 了解題目內容 3. **設計 Q 版本**:用「?」隱藏答案 4. **設計 A 版本**:完整顯示答案並加強調 5. **生成圖片**:使用 matplotlib 生成 6. **驗證大小**:確認 Q 和 A 檔案大小不同 7. **上傳伺服器**:scp 到圖片目錄 ### 批次修復腳本結構 ```python # -*- coding: utf-8 -*- import matplotlib.pyplot as plt from matplotlib.patches import FancyBboxPatch, Circle, Rectangle import os plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei', 'SimHei'] plt.rcParams['axes.unicode_minus'] = False OUTPUT_DIR = r"C:\Users\user\Documents\temp\images" os.makedirs(OUTPUT_DIR, exist_ok=True) FONT_TITLE = 42 FONT_LABEL = 32 FONT_TEXT = 28 FONT_SMALL = 24 def save_single(fig, filename): """儲存單一圖片""" filepath = os.path.join(OUTPUT_DIR, filename) fig.savefig(filepath, dpi=150, bbox_inches='tight', facecolor='white', edgecolor='none') print(f"已儲存: {filename}") plt.close(fig) def create_qXX_topic_Q(): """題目版 - 隱藏答案""" fig, ax = plt.subplots(figsize=(14, 10)) # ... 用「?」隱藏答案 ... save_single(fig, 'ch1-q9-topic.png') def create_aXX_topic_A(): """解答版 - 顯示答案""" fig, ax = plt.subplots(figsize=(14, 10)) # ... 顯示完整答案 ... save_single(fig, 'ch1-a9-topic.png') if __name__ == '__main__': create_qXX_topic_Q() create_aXX_topic_A() ``` ### 常見洩題類型與修復策略 | 題目類型 | Q 版本應隱藏 | A 版本應顯示 | |----------|--------------|--------------| | 回饋系統 | 隱藏「受器/控制中樞/動器」 | 顯示完整名稱和功能 | | 體腔分類 | 隱藏腔室名稱 | 顯示腔室名稱和包含器官 | | 肌肉收縮 | 隱藏哪個結構會縮短 | 標示 I帶/H帶 縮短 | | 動作電位 | 隱藏離子種類 | 標示 Na⁺ 流入 / K⁺ 流出 | | 突觸構造 | 隱藏囊泡名稱 | 標示「突觸囊泡」 | | 荷爾蒙機轉 | 隱藏作用機制 | 標示「第二傳訊者」 | ### 驗證修復成功 ```bash # 確認所有修復的圖片 Q/A 大小不同 ssh lt4 'cd /home/lt4.mynet.com.tw/public_html/linebot/images && \ ls -la ch1-q9*.png ch1-a9*.png' # 應該看到不同的檔案大小,例如: # -rw-r--r-- 1 root root 92287 ch1-q9-negative-feedback.png # -rw-r--r-- 1 root root 122518 ch1-a9-negative-feedback.png ``` ### 視覺審查頁面 建立 HTML 審查頁面,人工檢視所有 Q/A 圖片: ```html
📝 題目圖(應隱藏答案)
✅ 解答圖(顯示完整答案)