quiz-builder

Bot 名稱 Webhook URL config.php 位置

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "quiz-builder" with this command: npx skills add bingo-taiwan/claude-code-skills/bingo-taiwan-claude-code-skills-quiz-builder

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 對應

'chapters' => [ 'ch2-1-classification' => '2.1 物質的分類', 'ch3-4-atomic-number-mass' => '3.4 原子序與質量數', ]

注意:config.php 的 key 要與檔名前綴一致(不含 -quiz.json )

JSON 格式規範

題目檔 (*-quiz.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)

{ "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 題 需要圖片的題目

題目撰寫要點

  • 題幹清晰:避免歧義,一題一問

  • 選項對等:長度相近,格式一致

  • 干擾項合理:常見錯誤概念

  • 答案明確:只有一個最佳答案

圖片規範

圖片命名

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:規劃題目

章節:2.7 狀態變化

主題涵蓋

  • 熔化、凝固、汽化、凝結、昇華、凝華
  • 熔化熱、汽化熱
  • 加熱曲線
  • 相圖

題目分配

  • 定義題:10 題 (Q1-10)
  • 計算題:8 題 (Q11-18)
  • 應用題:8 題 (Q19-26)
  • 圖表題:4 題 (Q12, Q15, Q27, Q30)

Step 2:建立題目檔

使用 Write 工具建立 JSON:

檔案路徑

C:\Users\user\linebot-quiz\quiz\chemistry\ch2-7-state-changes-quiz.json

Step 3:建立答案檔

檔案路徑

C:\Users\user\linebot-quiz\quiz\chemistry\ch2-7-state-changes-answers.json

Step 4:驗證 JSON

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

'ch2-7-state-changes' => '2.7 狀態變化',

Step 6:推送到 GitHub

cd /c/Users/user/linebot-quiz git add . git commit -m "新增 2.7 狀態變化 (30題)" git push origin master

Step 7:部署到伺服器

同步題庫

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題)
  • 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 腳本自動檢測題目與答案的適配問題:

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 快取:

"question_image": "https://lt4.mynet.com.tw/linebot/images/ch7-q9-peptide-bond.png?v=1"

常見錯誤

題目文字方向性錯誤

問題:LINE Bot 的 Flex Message 中,圖片顯示在題目文字上方,但題目文字卻寫「下圖」。

// 錯誤:使用「下圖」 "question": "下圖顯示某些元素符號,標示 X 的元素是?"

// 正確:使用「上圖」 "question": "上圖顯示某些元素符號,標示 X 的元素是?"

批量修正:

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 標記):

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族)

範例:有邏輯的元素符號圖

按週期表第四週期順序排列,顯示原子序

elements = [ ('K', '鉀', '19'), ('Ca', '鈣', '20'), ('Fe', '鐵', '26'), ('Co', '鈷', '27'), ('Ni', '鎳', '28'), ('X', '?', '29'), # Cu 遮蓋成 X ('Zn', '鋅', '30'), ]

學生看到原子序 29,可推斷是 Cu(銅)

字體大小建議(PIL/Pillow):

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) # 題目文字

圖片與題目不匹配的排查

檢查清單:

  • 圖片中是否有題目描述的標記(X、箭頭、問號等)

  • 圖片中標記的位置是否對應正確答案

  • 題目文字的方向描述(上圖/下圖)是否正確

排查指令:

找出所有「標示 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 語法錯誤

// 錯誤:最後一項有逗號 {"id": 30, "answer": "C", "explanation": "..."}, ]

// 正確:最後一項無逗號 {"id": 30, "answer": "C", "explanation": "..."} ]

選項格式錯誤

// 錯誤:選項是陣列 "options": ["A選項", "B選項", "C選項", "D選項"]

// 正確:選項是物件 "options": {"A": "選項A", "B": "選項B", "C": "選項C", "D": "選項D"}

圖片路徑錯誤

// 錯誤:相對路徑 "question_image": "images/ch2-7-q12.png"

// 正確:完整 URL "question_image": "https://lt4.mynet.com.tw/linebot/images/ch2-7-q12-heating-curve.png"

範本

快速建立範本

複製此範本開始新章節:

{ "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 生成教育圖片:

-- 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 元件

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 背景/中性

多科目支援

目前支援科目

$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 # 人體生理學

新增科目步驟

  • 在 quiz/ 下建立科目目錄

  • 在 config.php 的 $SUBJECTS 新增科目設定

  • 建立題目和答案 JSON 檔案

  • 生成所需圖片並上傳

部署注意事項

SSH 連線設定

確保 ~/.ssh/config 有正確設定:

Host lt4 HostName 172.104.67.123 User root IdentityFile ~/.ssh/id_ed25519 IdentitiesOnly yes

批量上傳圖片

上傳特定章節圖片

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/

驗證上傳

ssh lt4 "ls /home/lt4.mynet.com.tw/public_html/linebot/images/ch*-a*.png | wc -l"

LINE 圖片快取問題與解決方案

問題描述

LINE 會積極快取圖片。當你更新伺服器上的圖片後,LINE 可能仍顯示舊版本(甚至空白圖片),因為 URL 沒變。

解決方案:快取破壞參數

在圖片 URL 後加入版本參數,強制 LINE 重新載入:

// 更新前 "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"

批量更新快取破壞參數

更新所有化學答案檔

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 正方形 圖標類

修改方式

// 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:

超大字體設定 (LINE Bot 手機閱讀優化)

FONT_TITLE = 48 # 標題 FONT_LABEL = 36 # 標籤 FONT_TEXT = 32 # 內文 FONT_SMALL = 28 # 小字(最小不要低於此)

save_fig 函數陷阱

問題:連續呼叫 save_fig 兩次會導致第二個檔案為空白!

錯誤示範

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 版本:

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)

完整範例腳本結構

-- 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()

故障排除

答案圖片不顯示

檢查 URL 是否正確:

ssh lt4 "grep 'explanation_image' /path/to/answers.json | head -3"

檢查圖片是否存在:

ssh lt4 "curl -I https://lt4.mynet.com.tw/linebot/images/ch2-2-a15-states-answer.png"

檢查圖片是否為空白(檔案很小可能是空白):

ssh lt4 "ls -la /path/to/image.png" # 小於 10KB 可能有問題 ssh lt4 "file /path/to/image.png" # 確認是有效 PNG

加入快取破壞參數:

ssh lt4 "sed -i 's/.png"/.png?v=2"/g' /path/to/answers.json"

圖片文字太小

  • 增加字體大小(至少 FONT_SMALL = 28)

  • 修改 webhook.php 的 aspectRatio 為 4:3

  • 重新生成圖片並上傳

  • 更新快取破壞參數

伺服器部署安全須知(重要!)

config.php 敏感資訊保護

問題:本地的 config.php 包含佔位符,伺服器上的 config.php 包含真實的 LINE 憑證。使用 scp 同步時會覆蓋伺服器上的真實憑證,導致 LINE Bot 完全失效。

症狀:

  • LINE Bot 完全沒有反應

  • 輸入任何文字都沒有回應

  • debug.log 顯示 Invalid signature

解決方案:

永遠不要直接同步 config.php 到伺服器:

危險!不要這樣做

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/

如果需要更新 config.php 的章節設定:

只更新章節設定,保留憑證

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"

備份伺服器 config.php:

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 重新取得:

// 伺服器上的 config.php 應該包含真實憑證 define('LINE_CHANNEL_ACCESS_TOKEN', '實際的token...'); define('LINE_CHANNEL_SECRET', '實際的secret');

LINE Bot 調試技巧

添加調試日誌

當 LINE Bot 沒有反應時,在 webhook.php 開頭添加日誌功能:

<?php // Debug 日誌 function logDebug($msg) { file_put_contents(DIR . '/debug.log', date('Y-m-d H:i:s') . ' ' . $msg . "\n", FILE_APPEND); }

// 在關鍵位置記錄 logDebug('=== Webhook called ==='); logDebug('Content: ' . substr($content, 0, 300));

在 replyMessages 添加 API 回應日誌

function replyMessages($replyToken, $messages) { // ... 原有代碼 ...

logDebug('Sending: ' . json_encode($data, JSON_UNESCAPED_UNICODE));

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
logDebug("LINE API Response (HTTP $httpCode): $response");

curl_close($ch);

}

常見錯誤與解決

日誌訊息 原因 解決方法

Invalid signature

LINE_CHANNEL_SECRET 錯誤 檢查 config.php 憑證

HTTP 400

訊息格式錯誤 檢查 Flex Message JSON

HTTP 401

ACCESS_TOKEN 錯誤 重新取得 token

沒有任何日誌 webhook URL 錯誤 檢查 LINE Console 設定

查看調試日誌

即時監控

ssh lt4 "tail -f /home/lt4.mynet.com.tw/public_html/linebot/debug.log"

查看最近 50 行

ssh lt4 "tail -50 /home/lt4.mynet.com.tw/public_html/linebot/debug.log"

清除日誌

ssh lt4 "echo '' > /home/lt4.mynet.com.tw/public_html/linebot/debug.log"

LINE Flex Message 注意事項

Button vs Box 的 Action

Button 組件(推薦):

  • 穩定可靠

  • label 有 20 字元限制

  • 適合短文字按鈕

[ 'type' => 'button', 'style' => 'primary', 'height' => 'sm', 'action' => [ 'type' => 'message', 'label' => '[1] 章節名稱', // 最多 20 字元 'text' => '1' ] ]

Box 組件的 Action:

  • 可以包含更長的文字(使用 wrap: true)

  • 某些情況下 action 可能不被觸發

  • 需要測試確認

[ 'type' => 'box', 'layout' => 'horizontal', 'contents' => [...], 'action' => [ 'type' => 'message', 'text' => '1' ] ]

長章節名稱處理

如果章節名稱超過 button label 限制:

// 截短名稱 $shortName = mb_strlen($name) > 12 ? mb_substr($name, 0, 12) . '..' : $name;

$buttons[] = [ 'type' => 'button', 'action' => [ 'type' => 'message', 'label' => "[{$i}] {$shortName}", 'text' => (string)$i ] ];

部署檢查清單

安全部署步驟

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 壞掉:

恢復 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"

伺服器修改最佳實踐(重要!)

每次修改後必做檢查

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 陣列!

危險操作(容易產生語法錯誤):

這樣刪除會留下多餘的括號!

ssh lt4 "sed -i "/'physiology' => [/,/]/d" config.php"

問題:sed 刪除多行 PHP 陣列時,容易留下多餘的 ] 或 , 導致語法錯誤。

安全做法:

用 vim 直接編輯:

ssh lt4 "vim /home/lt4.mynet.com.tw/public_html/linebot/config.php"

用 PHP 腳本修改:

ssh lt4 "php -r " \$config = file_get_contents('config.php'); // 做修改... file_put_contents('config.php', \$config); ""

完整重寫該段落(推薦):

先備份

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 科目設定的安全流程

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)

// 原本的 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 開頭

// 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 —

標準設定

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 最佳顯示尺寸:

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 標記

標籤範例:大字體 + 醒目背景

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 有新變更):

一行搞定:暫存 → 拉取 → 恢復 → 推送

git stash && git pull --rebase && git stash pop && git push

題目圖片洩題檢測與修復(2026-01-11 新增)

問題描述

題目圖片(Q版本)不應該顯示答案,應該用「?」隱藏答案,讓學生思考。如果 Q 和 A 圖片相同,等於直接洩漏答案。

洩題檢測方法

關鍵洞見:如果 Q 和 A 圖片的檔案大小完全相同,代表它們是同一張圖片(洩題)。

檢測所有 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):

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

  • 解答版

完整修復流程

  • 檢測洩題:比較 Q/A 檔案大小

  • 分析題目:讀取 JSON 了解題目內容

  • 設計 Q 版本:用「?」隱藏答案

  • 設計 A 版本:完整顯示答案並加強調

  • 生成圖片:使用 matplotlib 生成

  • 驗證大小:確認 Q 和 A 檔案大小不同

  • 上傳伺服器:scp 到圖片目錄

批次修復腳本結構

-- 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⁺ 流出

突觸構造 隱藏囊泡名稱 標示「突觸囊泡」

荷爾蒙機轉 隱藏作用機制 標示「第二傳訊者」

驗證修復成功

確認所有修復的圖片 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 圖片:

<div class="card"> <div class="images"> <div class="image-box"> <p>📝 題目圖(應隱藏答案)</p> <img src="https://lt4.mynet.com.tw/linebot/images/ch1-q9-negative-feedback.png"> </div> <div class="image-box"> <p>✅ 解答圖(顯示完整答案)</p> <img src="https://lt4.mynet.com.tw/linebot/images/ch1-a9-negative-feedback.png"> </div> </div> <div class="checklist"> <label><input type="checkbox"> 題目圖不洩題</label> <label><input type="checkbox"> 文字清晰無重疊</label> <label><input type="checkbox"> 圖片正常載入</label> </div> </div>

審查頁面 URL: https://lt4.mynet.com.tw/linebot/review.html

給 Claude 的執行指引

下次執行類似任務時,請遵循:

開工前先檢查版本同步狀態(最重要!)

用戶可能在家裡、公司、或直接在伺服器上修改程式碼。開始任何修改前,必須先確認版本一致:

檢查本地 Git 狀態

cd /c/Users/user/linebot-quiz git status git fetch origin git log HEAD..origin/master --oneline # 遠端有但本地沒有

比對本地和伺服器的 webhook.php

ssh lt4 "cat /home/lt4.mynet.com.tw/public_html/linebot/webhook.php" | head -20

與本地 webhook.php 比較關鍵部分

如果發現差異:

  • 提醒用戶:「發現本地/GitHub/伺服器版本不一致,請確認哪個是最新版本」

  • 不要貿然覆蓋任何版本

  • 讓用戶決定同步方向

修改伺服器檔案前先備份

ssh lt4 "cp file file.bak.$(date +%Y%m%d%H%M)"

每次修改後立即驗證 PHP 語法

ssh lt4 "php -l /path/to/file.php"

避免用 sed 刪除 PHP 陣列條目

  • 用簡單的文字替換(如改名稱)是安全的

  • 刪除整個陣列條目容易出錯,改用 vim 或完整重寫

LINE Bot 沒反應時的排查順序

  • 先檢查 HTTP 狀態碼(curl)

  • 如果 500 → 檢查 PHP 語法

  • 如果 400 → 檢查 debug.log

  • 如果日誌空 → 檢查 LINE webhook URL

不要直接 scp config.php

  • 會覆蓋伺服器上的 LINE 憑證

  • 改用 ssh + sed 或 vim 直接在伺服器修改

測試前清除 session

ssh lt4 "echo '{}' > /path/to/sessions.json"

LINE Flex Message 圖片最佳實踐(2026-01-12 新增)

核心原則:一開始就做對

避免「生成圖片 → 測試發現問題 → 修復」的循環,遵循以下規範一開始就產出正確的圖片。

圖片尺寸與 DPI 標準

設定 建議值 說明

figsize (14, 10)

英寸,約 2100×1500 像素 @150 DPI

DPI 150

平衡清晰度與檔案大小(單張約 60-200KB)

aspectRatio 4:3

LINE Flex Message 中圖片較高,文字更易讀

fig, ax = plt.subplots(figsize=(14, 10)) fig.savefig(filepath, dpi=150, bbox_inches='tight', facecolor='white')

字體大小標準(LINE Bot 手機可讀)

重要:LINE Bot 圖片無法放大,必須確保手機上可直接閱讀。

用途 字體大小 範例

主標題 48pt 圖片頂部標題

重要標記 48pt + 粗體 + 紅色 X/Y/Z 標記、關鍵元素

區塊標籤 36pt 方框內的標題

說明文字 32pt 一般解說

最小文字 28pt 次要資訊(絕對最小值!)

標準字體常數

FONT_TITLE = 48 # 標題 FONT_LABEL = 36 # 標籤 FONT_TEXT = 32 # 內文 FONT_SMALL = 28 # 小字(最小值)

防止答案洩漏的設計模式

模式一:X/Y/Z 標記法

適用於「比較類」題目,如:「下列何者為骨骼肌的特徵?」

Q 版本(題目):使用 X/Y/Z 隱藏名稱

muscles = [ (1.5, 'X', '#FFCDD2'), # 實際是骨骼肌 (5.5, 'Y', '#C8E6C9'), # 實際是心肌 (9.5, 'Z', '#BBDEFB'), # 實際是平滑肌 ] ax.text(x, y, name, fontsize=FONT_TITLE, fontweight='bold', color='#C62828')

A 版本(解答):顯示實際名稱

muscles = [ (1.5, '骨骼肌', '#FFCDD2', ['隨意控制', '有橫紋', '多核']), (5.5, '心肌', '#C8E6C9', ['不隨意', '有橫紋', '單核']), (9.5, '平滑肌', '#BBDEFB', ['不隨意', '無橫紋', '單核']), ]

模式二:中性標題法

適用於標題會洩漏答案的情況。

Q 版本:使用中性標題

ax.set_title('回饋機制示意圖', fontsize=FONT_TITLE) # 不說「負回饋」 ax.set_title('某內分泌腺構造圖', fontsize=FONT_TITLE) # 不說「腦下垂體」

A 版本:顯示完整標題

ax.set_title('負回饋調控示意圖', fontsize=FONT_TITLE) ax.set_title('腦下垂體 (Pituitary Gland)', fontsize=FONT_TITLE)

模式三:問號遮蔽法

適用於單一關鍵資訊需要隱藏的情況。

Q 版本:用「?」遮蔽答案

ax.text(x, y, '?\n調控機制', ha='center', fontsize=FONT_TEXT)

A 版本:顯示完整答案

ax.text(x, y, '負回饋\n調控機制', ha='center', fontsize=FONT_TEXT, bbox=dict(facecolor='#E8F5E9', edgecolor='#4CAF50')) # 綠框強調

常見洩題類型對照表

題目類型 洩題風險 Q 版本設計 A 版本設計

三種肌肉比較 直接顯示「骨骼肌」 用 X/Y/Z 標記 顯示「骨骼肌/心肌/平滑肌」

回饋系統類型 標題寫「負回饋」 標題改「回饋機制」 標題寫「負回饋調控」

內分泌腺識別 標題寫「腦下垂體」 標題改「某內分泌腺」 顯示「腦下垂體」

細胞構造識別 標籤寫「粒線體」 標籤改「?」或「構造 A」 顯示「粒線體」

離子識別 顯示「Na⁺」 顯示「X⁺」 顯示「Na⁺」

Q/A 版本生成函數模板

def create_CHAPTER_QNUM_TOPIC_QUESTION(): """題目版 - 隱藏答案""" 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)

# 用 X/Y/Z 或 ? 隱藏答案
ax.text(7, 5, 'X', fontsize=FONT_TITLE, fontweight='bold', color='#C62828')

save_single(fig, 'chN-qM-topic.png')

def create_CHAPTER_QNUM_TOPIC_ANSWER(): """解答版 - 顯示完整答案""" fig, ax = plt.subplots(figsize=(14, 10)) ax.set_xlim(0, 14) ax.set_ylim(0, 10) ax.axis('off')

# 顯示完整標題
ax.set_title('XXX 示意圖', fontsize=FONT_TITLE, fontweight='bold', pad=20)

# 顯示答案並強調
ax.text(7, 5, '答案', fontsize=FONT_LABEL, fontweight='bold',
        bbox=dict(facecolor='#E8F5E9', edgecolor='#4CAF50', linewidth=2))

save_single(fig, 'chN-aM-topic.png')

自動化圖片檢查流程

Playwright + LINE Flex Simulator 檢查

使用 Playwright 自動化在 LINE Flex Message Simulator 中預覽所有題目圖片:

line_quiz_checker.py 核心流程

from playwright.sync_api import sync_playwright

def run_checker(): # 1. 從伺服器取得所有有圖片的題目 questions = get_questions_with_images()

# 2. 啟動瀏覽器
with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()

    # 3. 開啟 LINE Flex Simulator
    page.goto('https://developers.line.biz/flex-simulator/')

    # 4. 等待登入(第一次需手動登入,之後用儲存的 session)
    if 'login' in page.url:
        wait_for_login(page)
        context.storage_state(path='line_auth_state.json')

    # 5. 逐一測試每個題目
    for q in questions:
        flex_json = generate_flex_json(q)
        # 輸入 JSON 並截圖
        page.click('button:has-text("View as JSON")')
        page.locator('textarea').fill(flex_json)
        page.click('button:has-text("Apply")')
        page.screenshot(path=f'{q["chapter"]}-q{q["id"]}.png')

VLM 圖片審查

用 Claude 的視覺能力檢查截圖是否有洩題問題:

讀取截圖並分析

from anthropic import Anthropic

def check_image_for_leakage(image_path, question_text): client = Anthropic()

with open(image_path, 'rb') as f:
    image_data = base64.b64encode(f.read()).decode()

response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=500,
    messages=[{
        "role": "user",
        "content": [
            {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": image_data}},
            {"type": "text", "text": f"""

檢查這張 LINE Bot 題目圖片是否有洩題問題。

題目:{question_text}

請檢查:

  1. 標題是否直接顯示答案?
  2. 圖片中的標籤是否洩漏答案?
  3. 文字是否清晰可讀?

回答格式:

  • 洩題風險:是/否
  • 問題描述:(如有)
  • 建議修正:(如有) """} ] }] ) return response.content[0].text

伺服器路徑注意事項

正確的檔案路徑

重要:lt4.mynet.com.tw 的 DocumentRoot 是 /home/lt4.mynet.com.tw/public_html/ ,不是 /var/www/html/ !

用途 正確路徑

LINE Bot 根目錄 /home/lt4.mynet.com.tw/public_html/linebot/

圖片目錄 /home/lt4.mynet.com.tw/public_html/linebot/images/

題庫目錄 /home/lt4.mynet.com.tw/public_html/linebot/quiz/

正確的上傳指令

scp images/*.png lt4:/home/lt4.mynet.com.tw/public_html/linebot/images/

錯誤!這個路徑不會被網頁伺服器服務

scp images/*.png lt4:/var/www/html/linebot/images/

CDN/快取問題處理

如果更新圖片後仍顯示舊版本,可能是 Apache mod_cache 快取問題:

1. 清除 Apache 快取

ssh lt4 "rm -rf /var/cache/apache2/mod_cache_disk/* && systemctl restart apache2"

2. 如果仍有問題,重新命名檔案

ssh lt4 "mv old.png new.png"

3. 更新 JSON 中的圖片 URL

ssh lt4 "sed -i 's/old.png/new.png/g' /path/to/*.json"

4. 或使用 cache-busting 參數

在 URL 後加上 ?v=2

"question_image": "https://lt4.mynet.com.tw/linebot/images/ch1-q9.png?v=2"

LINE Flex Message 圖片尺寸規範(2026-01-12 新增)

LINE 官方規範

項目 規範值 說明

基準寬度 1040px LINE Imagemap 和 Flex Message 的參考寬度

aspectRatio 4:3 webhook.php 中設定的圖片比例

最佳尺寸 1040 x 780 px 4:3 比例,符合 LINE 基準

檔案大小 < 1MB LINE 建議值,實際建議 < 300KB 以加速載入

為什麼不用 Gemini API 製圖

經過多次測試,不建議使用 Gemini API 生成教育圖片,原因:

  • 中文字模糊:AI 生成的中文字經常模糊、變形或出現錯字

  • 無法控制尺寸:無法精確指定輸出尺寸(如 1040x780)

  • 字體不可控:無法指定使用正體中文字體

  • 結果不穩定:每次生成結果不同,難以維持一致品質

建議方案:使用 matplotlib + 微軟正黑體 製圖,完全可控。

matplotlib 製圖特殊字符問題(2026-01-12 新增)

問題:方框字 (□) 出現

使用微軟正黑體時,某些 Unicode 特殊字符會顯示為方框:

問題字符 Unicode 顯示結果 解決方案

− (minus sign) U+2212 □ 改用 - (普通連字符)

₂ (subscript 2) U+2082 □ 改用 2 (普通數字)

₃ (subscript 3) U+2083 □ 改用 3 (普通數字)

α (alpha) U+03B1 ✓ 正常 可以使用

錯誤示範 vs 正確做法

錯誤:使用特殊 Unicode 字符

ax.text(x, y, '−NH₂', fontsize=26) # 顯示為 □NH□ ax.text(x, y, '−COOH', fontsize=26) # 顯示為 □COOH

正確:使用普通 ASCII 字符

ax.text(x, y, '-NH2', fontsize=26) # 正常顯示 ax.text(x, y, '-COOH', fontsize=26) # 正常顯示

檢查腳本是否有問題字符

檢查 Python 腳本中是否有問題字符

grep -n '[−₀₁₂₃₄₅₆₇₈₉]' your_script.py

執行時的警告訊息

如果看到以下警告,表示有特殊字符問題:

UserWarning: Glyph 8722 (\N{MINUS SIGN}) missing from font(s) Microsoft JhengHei. UserWarning: Glyph 8322 (\N{SUBSCRIPT TWO}) missing from font(s) Microsoft JhengHei.

matplotlib 製圖完整範本(1040x780 優化版)

標準設定

-- coding: utf-8 --

import matplotlib.pyplot as plt from matplotlib.patches import FancyBboxPatch, Circle import os

中文字體設定(必須)

plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei', 'SimHei', 'Arial Unicode MS'] plt.rcParams['axes.unicode_minus'] = False # 避免負號問題

輸出目錄

OUTPUT_DIR = r"C:\Users\user\quiz_images" os.makedirs(OUTPUT_DIR, exist_ok=True)

圖片尺寸 (1040x780 at 100 DPI = 10.4x7.8 inches)

FIG_W, FIG_H = 10.4, 7.8 DPI = 100

字體大小(LINE Bot 手機可讀)

FONT_TITLE = 42 # 主標題 FONT_LARGE = 32 # 重要標籤 FONT_MEDIUM = 26 # 一般文字 FONT_SMALL = 22 # 次要資訊(最小值!)

Material Design 配色

COLORS = { 'blue': '#1565C0', 'light_blue': '#BBDEFB', 'green': '#2E7D32', 'light_green': '#C8E6C9', 'red': '#C62828', 'light_red': '#FFCDD2', 'orange': '#E65100', 'light_orange': '#FFE0B2', 'purple': '#7B1FA2', 'light_purple': '#E1BEE7', 'gray': '#607D8B', 'light_gray': '#ECEFF1', }

儲存函數

def save_fig(fig, filename): """儲存圖片 - 1040x780, 優化檔案大小""" filepath = os.path.join(OUTPUT_DIR, filename) fig.savefig(filepath, dpi=DPI, bbox_inches='tight', facecolor='white', edgecolor='none', pad_inches=0.1) plt.close(fig) size_kb = os.path.getsize(filepath) / 1024 print(f" [OK] {filename} ({size_kb:.0f} KB)")

完整範例:胺基酸結構圖

def create_amino_acid_structure(): """胺基酸結構圖 - 1040x780""" fig, ax = plt.subplots(figsize=(FIG_W, FIG_H)) ax.set_xlim(0, 10.4) ax.set_ylim(0, 7.8) ax.axis('off') ax.set_facecolor('white')

# 標題
ax.text(5.2, 7.2, '胺基酸基本結構', fontsize=FONT_TITLE, fontweight='bold',
        ha='center', va='center', color=COLORS['blue'])

# 中心 - α碳
center_x, center_y = 5.2, 4.0
circle = Circle((center_x, center_y), 0.6, facecolor=COLORS['light_blue'],
                edgecolor=COLORS['blue'], linewidth=3)
ax.add_patch(circle)
ax.text(center_x, center_y, 'C', fontsize=FONT_LARGE, fontweight='bold',
        ha='center', va='center', color=COLORS['blue'])

# 左邊 - 胺基(注意:使用普通連字符和數字!)
ax.add_patch(FancyBboxPatch((1.5, 3.2), 2.0, 1.6, boxstyle="round,pad=0.1",
                            facecolor=COLORS['light_green'], edgecolor=COLORS['green'], linewidth=2))
ax.text(2.5, 4.0, '胺基', fontsize=FONT_LARGE, fontweight='bold', ha='center', va='center')
ax.text(2.5, 3.5, '-NH2', fontsize=FONT_MEDIUM, ha='center', va='center', color=COLORS['green'])
# 注意:用 '-NH2' 而不是 '−NH₂'

# 右邊 - 羧基
ax.add_patch(FancyBboxPatch((6.9, 3.2), 2.0, 1.6, boxstyle="round,pad=0.1",
                            facecolor=COLORS['light_red'], edgecolor=COLORS['red'], linewidth=2))
ax.text(7.9, 4.0, '羧基', fontsize=FONT_LARGE, fontweight='bold', ha='center', va='center')
ax.text(7.9, 3.5, '-COOH', fontsize=FONT_MEDIUM, ha='center', va='center', color=COLORS['red'])
# 注意:用 '-COOH' 而不是 '−COOH'

save_fig(fig, 'ch7-q1-protein.png')

預期輸出

項目 數值

圖片尺寸 1040 x 780 px

檔案大小 50-110 KB

載入時間 < 0.5 秒

Gemini API 題目生成(文字部分仍可用)

雖然 Gemini API 不適合生成圖片,但生成題目文字仍然有效:

分批生成避免截斷

Gemini 生成大量題目時可能被截斷,建議分批處理:

分 5 批,每批 10 題

for batch in range(1, 6): start_id = (batch - 1) * 10 + 1 questions = await generate_batch(batch, start_id, 10, session) all_questions.extend(questions) await asyncio.sleep(1) # 避免 rate limit

JSON 解析修復

Gemini 回傳的 JSON 可能有格式問題:

修復常見 JSON 錯誤(多餘逗號)

json_str = re.sub(r',(\s*[}]])', r'\1', json_str)

try: data = json.loads(json_str) except json.JSONDecodeError as e: # 儲存原始回應供除錯 with open(f'debug_batch{batch}.txt', 'w', encoding='utf-8') as f: f.write(response)

圖片生成品質檢查清單

生成圖片後,逐一確認以下項目:

可讀性檢查

  • 標題文字 ≥ 48pt

  • 標籤文字 ≥ 36pt

  • 最小文字 ≥ 28pt

  • 在手機上模擬預覽(實際大小約 350×263 像素)

洩題檢查

  • Q 版本標題不含答案關鍵字

  • Q 版本使用 X/Y/Z 或 ? 隱藏答案

  • Q 版本和 A 版本檔案大小不同(相同 = 洩題)

技術檢查

  • 檔案大小在 50-250KB 範圍

  • 圖片比例接近 4:3

  • 上傳到正確路徑(/home/lt4.mynet.com.tw/...)

  • URL 可正常存取(curl -I 測試)

快速驗證指令

檢查 Q/A 檔案大小是否不同(相同 = 洩題)

ssh lt4 'cd /home/lt4.mynet.com.tw/public_html/linebot/images &&
for q in ch*-q*.png; do a=$(echo "$q" | sed "s/-q/-a/") if [ -f "$a" ]; then qs=$(stat -c%s "$q") as=$(stat -c%s "$a") if [ "$qs" = "$as" ]; then echo "⚠️ 洩題風險: $q ($qs) = $a" fi fi done'

檢查圖片是否正常載入

curl -s -o /dev/null -w "%{http_code}" https://lt4.mynet.com.tw/linebot/images/ch1-q9-negative-feedback.png

應該返回 200

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

github-workflow

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

docx-format-replicator

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

fanpage-article-writer

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

video processor

No summary provided by upstream source.

Repository SourceNeeds Review