quiz-builder
npx skills add https://github.com/bingo-taiwan/claude-code-skills --skill quiz-builder
Agent 安装分布
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 é¡ | éè¦åççé¡ç® |
é¡ç®æ°å¯«è¦é»
- é¡å¹¹æ¸ æ°ï¼é¿å æ§ç¾©ï¼ä¸é¡ä¸å
- é¸é å°çï¼é·åº¦ç¸è¿ï¼æ ¼å¼ä¸è´
- å¹²æ¾é åçï¼å¸¸è¦é¯èª¤æ¦å¿µ
- çæ¡æç¢ºï¼åªæä¸åæä½³çæ¡
åçè¦ç¯
åçå½å
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) # é¡ç®æå
åçèé¡ç®ä¸å¹é çææ¥
æª¢æ¥æ¸ å®ï¼
- åç䏿¯å¦æé¡ç®æè¿°çæ¨è¨ï¼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 éæ°åå¾ï¼
- ç¶²åï¼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 é£åæï¼å®¹æçä¸å¤é¤ç ] æ , å°è´èªæ³é¯èª¤ã
å®å ¨åæ³ï¼
-
ç¨ 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 çæç䏿åç¶å¸¸æ¨¡ç³ãè®å½¢æåºç¾é¯å
- ç¡æ³æ§å¶å°ºå¯¸ï¼ç¡æ³ç²¾ç¢ºæå®è¼¸åºå°ºå¯¸ï¼å¦ 1040×780ï¼
- åé«ä¸å¯æ§ï¼ç¡æ³æå®ä½¿ç¨æ£é«ä¸æåé«
- çµæä¸ç©©å®ï¼æ¯æ¬¡çæçµæä¸åï¼é£ä»¥ç¶æä¸è´å質
å»ºè°æ¹æ¡ï¼ä½¿ç¨ 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