-
Notifications
You must be signed in to change notification settings - Fork 2
Maintenance
日常維護這套系統需要知道的事。詳細的初次部署流程見 repo 內 CLAUDE.md("setup / deploy" 一節)與 README.md。
./scripts/setup.sh # 首次:互動式產生 config.toml / wrangler.toml / .env
pnpm install
cd frontend && pnpm install && cd ..
pnpm db:migrate:local # 本地 D1 schema + 樣本資料
pnpm dev # terminal A:wrangler dev (:8787)
cd frontend && pnpm dev # terminal B:vite (:5173)- Vite proxy 會把
/api、/img轉到 :8787 並注入X-Dev-Email;.dev.vars的CF_ACCESS_TEAM_DOMAIN=localhost啟用 Access bypass。 - 改本機登入 email:
config.toml [dev] dev_email。 - 常見坑:port 8787 被占走。若本地所有 API 回 500/404,先查是不是 OpenEvidence MCP relay daemon 佔了 8787,不是程式碼問題。
- 本地 D1 在
.wrangler/state/v3/d1/;砍掉重來:rip .wrangler/state。
./scripts/deploy.sh # D1 / R2 / Worker / Pages 一鍵,idempotent
node --experimental-strip-types scripts/sync-access.ts # Access app + policy + Worker secrets + users 種子
./scripts/setup-public-bypass.sh # landing / OG / favicon 等 path-scoped bypass.env 需要 CF_API_TOKEN、CF_ACCOUNT_ID、PAGES_DOMAIN、ADMIN_EMAILS、ROSTER_CSV_URL(範本見 .env.example)。部署失敗多半是 API token 缺 scope、zone 不在帳號下、或資源名衝突——讀錯誤訊息,不要跳步。
兩個部署前必check:
# 1. wrangler 只認 CLOUDFLARE_API_TOKEN,.env 用的是舊名 CF_API_TOKEN
# 少這行會掛在「Failed to fetch auth token: 400」,看起來像 token 過期
export CLOUDFLARE_API_TOKEN="$CF_API_TOKEN" CLOUDFLARE_ACCOUNT_ID="$CF_ACCOUNT_ID"
# 2. 部署後確認 Pages 上的是 Production 不是 preview(頂列必須是 Production │ main)
wrangler pages deployment list --project-name hema-2026 | head -4第 2 點特別容易在 worktree 裡踩到:wrangler pages deploy 從 branch name 推環境,而 worktree 永遠不在 main 上。Worker 不看 branch 照樣上線,結果是「新 Worker + 舊前端」,症狀看起來像快取問題。要嘛從主工作區的 main 部署,要嘛給 Pages 那步加 --branch main。
wrangler d1 migrations create <db> <name> # 新增 migration(絕不改已套用的檔)
pnpm db:migrate:local # 先本地測
pnpm db:migrate:remote # 再上 prod
pnpm db:pull # 鏡像 remote → local(單向,local 會被覆蓋)檢視資料:wrangler d1 execute hema-2026-db --local --command "SELECT ..."(prod 加 --remote)。
CSV 格式:year,number,group,stem,option_a..e,answer,tags,difficulty,source;year 用民國(104–114),group ∈ {內科, 共同}。
node --experimental-strip-types scripts/import-questions.ts ./questions.csv # prod
node --experimental-strip-types scripts/import-questions.ts ./questions.csv --local # 本地- Pre-flight 驗證整批(年份範圍、1–70 內科 / 71–100 共同、(year,number) 唯一、answer 對應存在的選項),任一筆失敗整批拒絕。
- 重要:importer 不會覆蓋社群已修訂的答案(answer challenge 升級過的題目)。這是修過的 bug,改 importer 時務必保住這個行為。
- 名單來源是 Google Sheet 發佈的 CSV(
ROSTER_CSV_URL),email 欄位目前寫死在 column index 3(scripts/sync-access.ts:194)。 - 每日 cron 會自動同步 roster → Access policy + D1
users;手動跑:pnpm sync-users。 - 新成員上線後即出現在 @mention picker(sync 會預先 seed
users列)。
pnpm import:lectures # PDF → R2 + lecture_docs 登錄
pnpm seed:lecture-notes # 預設頁面筆記| 需求 | 指令 / 位置 |
|---|---|
| Prod Worker log |
pnpm tail(wrangler tail) |
| Pages log | Dashboard → Pages → 專案 → Functions |
| 單元測試 |
pnpm test(node --test,涵蓋 worker 與前端的純函式;整合層無測試,見 技術債 第 1 項) |
| 型別檢查 | pnpm typecheck |
| 跨瀏覽器驗證 | 見下節——改前端一定要跑,Chromium 過不代表 Safari 過 |
| 本地 DB 檢視 | wrangler d1 execute <db> --local --command ... |
2026-07-29 之前,所有 iOS 使用者的題目頁都是白畫面,而沒有任何一道防線發現——因為驗證一直只跑 Chromium。iOS 強制所有瀏覽器使用 WebKit,所以「在 Chrome 上好好的」完全不構成證據(成因見 踩過的坑 十四)。
pnpm add -D playwright && pnpm exec playwright install webkit要點:
- 打正式建置產物,不是 dev server。dev 有 React StrictMode 雙掛載,訊號不一樣(會多冒
Adding different instances of a keyed plugin,那是 StrictMode 的產物不是根因)。 - 打
deploy.sh實際產出的那份dist。worktree 與主工作區的node_modules解析版本可能不同,同一份原始碼會建出不同 bundle。 - 用
devices['iPhone 13'],並監聽pageerror——白屏的特徵是document.body.innerText.length === 0且有一筆 pageerror。
const b = await webkit.launch()
const page = await (await b.newContext(devices['iPhone 13'])).newPage()
const errs = []
page.on('pageerror', e => errs.push(e.message))
await page.goto(`${BASE}/q/113-050`, { waitUntil: 'load' })
await page.waitForTimeout(3500)
// 斷言:innerText.length > 0 且 errs.length === 0本機要同時跑 API:起 wrangler dev,再用一支靜態伺服器服務 dist 並把 /api、/img 代理過去(注入 X-Dev-Email)。
影片掛在主題上不掛題目(video_topics ← tag_topics ← question_tags),所以補 tag 就會自動生效,不必重跑策展。主題白名單在 scripts/video-topics.json。
python3 scripts/curate-videos.py search # yt-dlp 搜尋 + 過濾 → 候選(有快取,可中斷續跑)
python3 scripts/score-batches.py split # 切批次交給 Claude Code 派 Haiku 評分
python3 scripts/score-batches.py merge # 合併評分(需 opencc 做繁體化)
python3 scripts/curate-videos.py publish --remote # 每主題取前 8 → 縮圖進 R2 → 寫 D1
python3 scripts/curate-videos.py refresh --remote # 只重抓 metadata,下架的標 status='dead'- 快取在
scripts/data/(gitignored)。留著它,重跑或調門檻不必再打一次 YouTube(約 30 分鐘)。 -
publish重跑不會復活使用者刪掉的影片(status刻意不在 upsert 的更新欄位裡)。 - 沒有 YouTube API key,走 yt-dlp。它哪天被 YouTube 反制就會壞,但只影響這支離線腳本,站上資料不受影響。
- R2 bucket 永不公開;圖片/PDF 一律走 Worker proxy。
- 不加 app 層 auth;身分只來自 Access JWT 的 email。
- DB 只存 TipTap JSON,渲染只走 read-only TipTap。
- 錯誤回應不外洩內部細節(已修過 error-detail leakage,維持住)。