quiz-builder

📁 bingo-taiwan/claude-code-skills 📅 Jan 25, 2026
8
总安装量
8
周安装量
#36011
全站排名
安装命令
npx skills add https://github.com/bingo-taiwan/claude-code-skills --skill quiz-builder

Agent 安装分布

claude-code 6
gemini-cli 6
codex 5
windsurf 5
opencode 5
openclaw 4

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

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

題目撰寫要點

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

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

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

圖片與題目不匹配的排查

檢查清單:

  1. 圖片中是否有題目描述的標記(X、箭頭、問號等)
  2. 圖片中標記的位置是否對應正確答案
  3. 題目文字的方向描述(上圖/下圖)是否正確

排查指令:

# 找出所有「標示 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   # 人體生理學

新增科目步驟

  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

批量上傳圖片

# 上傳特定章節圖片
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()

故障排除

答案圖片不顯示

  1. 檢查 URL 是否正確:

    ssh lt4 "grep 'explanation_image' /path/to/answers.json | head -3"
    
  2. 檢查圖片是否存在:

    ssh lt4 "curl -I https://lt4.mynet.com.tw/linebot/images/ch2-2-a15-states-answer.png"
    
  3. 檢查圖片是否為空白(檔案很小可能是空白):

    ssh lt4 "ls -la /path/to/image.png"  # 小於 10KB 可能有問題
    ssh lt4 "file /path/to/image.png"    # 確認是有效 PNG
    
  4. 加入快取破壞參數:

    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 到伺服器:

    # 危險!不要這樣做
    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 的章節設定:

    # 只更新章節設定,保留憑證
    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:

    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
// 伺服器上的 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 陣列時,容易留下多餘的 ] 或 , 導致語法錯誤。

安全做法:

  1. 用 vim 直接編輯:

    ssh lt4 "vim /home/lt4.mynet.com.tw/public_html/linebot/config.php"
    
  2. 用 PHP 腳本修改:

    ssh lt4 "php -r \"
    \\\$config = file_get_contents('config.php');
    // 做修改...
    file_put_contents('config.php', \\\$config);
    \""
    
  3. 完整重寫該段落(推薦):

    # 先備份
    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 – 解答版

完整修復流程

  1. 檢測洩題:比較 Q/A 檔案大小
  2. 分析題目:讀取 JSON 了解題目內容
  3. 設計 Q 版本:用「?」隱藏答案
  4. 設計 A 版本:完整顯示答案並加強調
  5. 生成圖片:使用 matplotlib 生成
  6. 驗證大小:確認 Q 和 A 檔案大小不同
  7. 上傳伺服器: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 的執行指引

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

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

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

    # 檢查本地 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/伺服器版本不一致,請確認哪個是最新版本」
    • 不要貿然覆蓋任何版本
    • 讓用戶決定同步方向
  2. 修改伺服器檔案前先備份

    ssh lt4 "cp file file.bak.$(date +%Y%m%d%H%M)"
    
  3. 每次修改後立即驗證 PHP 語法

    ssh lt4 "php -l /path/to/file.php"
    
  4. 避免用 sed 刪除 PHP 陣列條目

    • 用簡單的文字替換(如改名稱)是安全的
    • 刪除整個陣列條目容易出錯,改用 vim 或完整重寫
  5. LINE Bot 沒反應時的排查順序

    • 先檢查 HTTP 狀態碼(curl)
    • 如果 500 → 檢查 PHP 語法
    • 如果 400 → 檢查 debug.log
    • 如果日誌空 → 檢查 LINE webhook URL
  6. 不要直接 scp config.php

    • 會覆蓋伺服器上的 LINE 憑證
    • 改用 ssh + sed 或 vim 直接在伺服器修改
  7. 測試前清除 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 生成教育圖片,原因:

  1. 中文字模糊:AI 生成的中文字經常模糊、變形或出現錯字
  2. 無法控制尺寸:無法精確指定輸出尺寸(如 1040×780)
  3. 字體不可控:無法指定使用正體中文字體
  4. 結果不穩定:每次生成結果不同,難以維持一致品質

建議方案:使用 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 製圖完整範本(1040×780 優化版)

標準設定

# -*- 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