بنقرة واحدة
deploy
引導遊戲部署到自托管 Supabase + Cloudflare Tunnel 的單一 production 主機(首次設定/例行更新/rollback/smoke 驗收)
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
引導遊戲部署到自托管 Supabase + Cloudflare Tunnel 的單一 production 主機(首次設定/例行更新/rollback/smoke 驗收)
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
End-to-end Taiwan anti-scam knowledge pipeline for Codex agents. Use when crawling or updating Taiwan fraud sources, using Playwright CLI to inspect or fallback on dynamic pages, classifying records into the five major fraud categories, validating classified JSONL, ingesting validated records into PostgreSQL JSONB, normalizing into relational tables, chunking text, and writing embeddings to pgvector.
解除分期付款(ATM)詐騙的角色、手法與話術知識庫;含可載入的對話人格(假客服/真客服)。
假網路拍賣(假得標通知/假客服)的角色、手法與話術知識庫;含可載入的對話人格(假拍賣客服/真平台客服)。
投資詐欺的角色、手法與話術知識庫;含可載入的對話人格(詐騙者/合法對照)。
假愛情交友詐騙的角色、手法與話術知識庫;含可載入的對話人格(假交友對象/真誠交友對象)。
一般購物詐欺(偽稱買賣)的角色、手法與話術知識庫;含可載入的對話人格(詐騙賣家/合法平台賣家)。
| name | deploy |
| description | 引導遊戲部署到自托管 Supabase + Cloudflare Tunnel 的單一 production 主機(首次設定/例行更新/rollback/smoke 驗收) |
用於引導 antifraud_game 部署到單一 production 主機,搭配自托管 Supabase、Cloudflare Tunnel、production compose 與部署/回滾腳本。
以下四項必須先確認完成並機器驗證通過,才動任何基礎設施。它們無法由腳本代勞,而且缺任何一項都會讓部署跑到最後一步才失敗、留下半啟動的容器。
| # | 前置 | 驗證指令 | 期望 |
|---|---|---|---|
| 1 | Docker 可用 | docker compose version | 有版本輸出 |
| 2 | Cloudflare Tunnel 已建,兩條 Public Hostname 都設好 | dig +short <domain> 與 dig +short api.<domain> | 兩者都要有 A 記錄 |
| 3 | 兩個 .env 已填 | grep -c '^CLOUDFLARE_TUNNEL_TOKEN=.\+' .env | 1 |
| 4 | 遊戲內容種子檔在 repo 內 | wc -l deploy/seed/game_cases.sql | 非空(部署後跑 seed-game-cases.sh 灌入) |
api.<domain>這條 DNS 特別容易漏。 前端映像把VITE_API_URL=https://api.<domain>build 時 baked 進靜態 JS,runtime 改不了。少了這條記錄,前端會正常載入,但每一個 API 呼叫都失敗——症狀像前端壞了,其實是 DNS。Public Hostnames 的 origin URL 是 Docker compose service 名(
http://frontend:80/http://backend:8000),由 cloudflared 透過 Docker 內建 DNS(127.0.0.11)解析。打錯字(例如rontend)的症狀是502加上日誌裡lookup rontend ... no such host,不是 404。
金鑰不要貼進聊天或 shell history。直接寫進 gitignored 的 .env:
python3 - <<'EOF'
import pathlib, getpass
p = pathlib.Path(".env"); tok = getpass.getpass("Tunnel token: ")
p.write_text("".join(
f"CLOUDFLARE_TUNNEL_TOKEN={tok}\n" if l.startswith("CLOUDFLARE_TUNNEL_TOKEN=") else l
for l in p.read_text().splitlines(keepends=True)))
EOF
docker compose 可用。deploy/cloudflared/README.md 建立 cloudflared tunnel,並在 Cloudflare dashboard 設定兩條 Public Hostnames ingress。用上表的 dig 驗證。deploy/supabase/README.md 取得官方 stack 並啟動自托管 Supabase compose。
驗證:docker ps --filter label=com.docker.compose.project=supabase 應全部 healthy。.env(金鑰、JWT、跨檔一致性、逐欄說明見 references/env-vars-and-secrets.md):
deploy/supabase/.env:Supabase stack 金鑰。用上游自帶的官方產生器:sh stack/utils/generate-keys.sh --update-env(JWT_KEYS / JWT_JWKS / SUPABASE_*_KEY 留空即為該版本預設,遊戲不使用)。.env.example 為 .env,填 production 真值(SECRET_KEY、GOOGLE_API_KEY、Supabase 連線、DOCKER_IMAGE_*、TAG、CLOUDFLARE_TUNNEL_TOKEN 等)。POSTGRES_PASSWORD 與 POSTGRES_USER=postgres.<tenant-id> 兩檔必須對上(tenant-id = supabase .env 的 POOLER_TENANT_ID)。TAG 要 pin 明確版本(short-sha 或日期),不要用浮動 latest。bash .claude/skills/deploy/scripts/check-image.sh <owner>/<repo>-backend <TAG>(frontend 同樣跑一次)。
失敗時先讀「鏡像疑難排解」章節,不要急著 docker login。bash deploy/scripts/seed-game-cases.sh(需 Supabase 已起、根 .env 已填)。
跳過這步的話,部署會成功、健康檢查會過,但 quiz 與 scenario 會 500。詳見「內容供給」章節。bash deploy/scripts/deploy.sh。
prestart 會自動跑 alembic upgrade head,並依 FIRST_SUPERUSER / FIRST_SUPERUSER_PASSWORD 建立初始 superuser。不需要手動建。deploy.sh 會自 .env 讀 DOMAIN(shell 環境變數優先)。deploy.sh 自己會等 backend healthy 並做對外健康檢查,印出 ✓ 部署完成 才算過。
接著跑「上線後 smoke 清單」——健康檢查通過不代表遊戲能玩(見該節說明)。
documents / game_cases / document_chunks 是管線表,被 backend/app/alembic/env_filters.py 的 include_object 白名單刻意排除——Alembic 永遠不會建立或修改它們,prestart.sh 也只跑 alembic upgrade head。
代價:全新的 production DB 上 game_cases 不存在。 部署會成功、deploy.sh 會印 ✓ 部署完成、健康檢查會過,但玩家一進 quiz 或 scenario 就 500:
sqlalchemy.exc.ProgrammingError: (psycopg.errors.UndefinedTable)
relation "game_cases" does not exist
所以首次部署必須先灌內容(deploy.sh 之前或之後都行,只要在玩之前):
bash deploy/scripts/seed-game-cases.sh
deploy/seed/game_cases.sql——由策展環境 pg_dump --table=public.game_cases 匯出,40 筆 status='published'(5 種 fraud_type × 4 詐騙 + 4 正常對照)。game_cases 一張表。backend/app/core/cases.py 是唯一讀取層,只讀這張;documents / document_chunks(含 embedding)純屬策展管線,production runtime 不需要,故不進 repo。published 資料就跳過。要重灌用 FORCE=1(會 DROP TABLE ... CASCADE)。supavisor:5432 + postgres.<tenant-id>,讀根 .env 的 POSTGRES_*,與遊戲 backend 完全同一條路徑。策展環境(跑過 data_pipeline/ 管線、並在 Studio 人工把 draft 升為 published 的那套 DB)重新匯出即可:
docker exec supabase-db pg_dump -U postgres -d postgres \
--table=public.game_cases --no-owner --no-privileges --no-comments \
> deploy/seed/game_cases.sql
然後在 production 主機 git pull 後跑 FORCE=1 bash deploy/scripts/seed-game-cases.sh。
為什麼不用
data_pipeline/data/manual/seed_game_cases.jsonl?那 40 筆草稿不能獨立灌入:ingest_game_cases.py會驗證每筆的source_document_ids(如[70]、[76])在documents表裡存在,否則 abort;而且它只寫入status='draft',發布到published是 Studio 上的人工步驟。那條路徑屬於策展環境,不是部署路徑。
git pull。.env 的 TAG 更新到要上線的版本(short-sha 或日期)。bash deploy/scripts/deploy.sh。docker pull $DOCKER_IMAGE_BACKEND:<tag>。TAG=<前一個良好鏡像 tag> bash deploy/scripts/rollback.sh。⚠️ DB 遷移不會自動 downgrade。若本次上線含破壞性遷移,回退前要人工評估。
⚠️ 舊 tag 不保證永遠可拉——見下節。回滾前務必先驗
docker pull。
manifest unknownbuild.yml 各架構分開 build 並以 push-by-digest=true 推送不帶 tag 的 manifest,再由 merge job 建立 OCI image index 指向它們。tag 只掛在 index 上;各架構的子 manifest 永遠是 untagged。
因此:絕對不要在 GitHub Packages 頁面刪除 "untagged" 版本。 刪了之後每一個 tag 都變成懸空 index——GET manifests/<tag> 仍回 200,但所有子 manifest 與 blob 都 404,docker pull 報 manifest unknown。這對所有歷史 tag 同時生效,rollback.sh 會一起失效。
診斷(別只驗第一層——index 回 200 不代表鏡像可用):
bash .claude/skills/deploy/scripts/check-image.sh <owner>/<repo>-backend <tag>
exit 0 = index 與所有子 manifest 完整;exit 1 = 懸空或取不到。子 manifest 若為 404 →
重跑 gh workflow run build.yml --ref main。注意:重跑只會修好該次 run 產生的 tag,舊 tag 仍指向舊的懸空 index,永遠救不回來。
manifest unknown 也可能是真的沒權限(private 鏡像未 docker login ghcr.io)。分辨方式:
GHCR 對匿名未授權回 401,對「已認證但無此物」回 404;上面的腳本取不到 pull token 時會直接提示。
逐條實際操作,不要只看 HTTP 狀態碼。quick/swipe/deck、pretest/questions、mascot/*、economy/* 即使沒灌 game_cases 也全部回 200——它們讀的是 init_db()(由 prestart 呼叫)自動 seed 的 swipe_card / pretest_question / mascot_item / property_tier。只有 quiz 與 scenario 依賴 game_cases,也只有這兩條會暴露內容缺口。
https://<domain> 前端可載入。https://api.<domain>/docs 可達,Swagger 正常顯示。FIRST_SUPERUSER)。.env 需設定 GOOGLE_API_KEY。任一步失敗時先看 docker compose -f deploy/compose.prod.yml --project-directory . logs --tail 50 backend;
relation "game_cases" does not exist → 回到「內容供給」章節。