Appearance
本教程演示如何从 0 创建一个最小可用的“关卡插件”(一个关卡模块),并接入关卡门禁与发放 token 的流程。
1. 创建关卡目录
在项目根目录下创建:
levels/<module>/
└── __init__.py其中:
<module>:Python 包名(建议与slug一致,如cipher)
也可以用 TUI 生成模板:
bash
python manage.py在 TUI 里按 n 新建关卡,会自动生成 levels/<module>/__init__.py 并把条目追加到 config/map.json。
2. 在 config/map.json 注册关卡
打开 config/map.json,在 levels 数组中追加一项:
json
{ "slug": "cipher", "module": "cipher", "name": "凯撒", "enabled": true }要点:
- 数组顺序就是等级顺序:索引 0 → Lv0(免 token),索引 1 → Lv1(需要 token)……
slug决定访问路径:/cipher(静态页)与/api/cipher(API)
3. 编写最小关卡代码(router + verify)
在 levels/cipher/__init__.py 中提供:
router = APIRouter()set_level_index(index: int)或CURRENT_LEVEL_INDEX(用于计算下一关)GET /:返回页面(HTML)POST /verify:校验答案并发 token
参考模板(与 manage.py 生成的结构一致):
py
from uuid import uuid4
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from core.auth import generate_token
from core.config import next_level_slug
router = APIRouter()
CURRENT_LEVEL_INDEX = 0
def set_level_index(index: int) -> None:
global CURRENT_LEVEL_INDEX
CURRENT_LEVEL_INDEX = index
@router.get("/", response_class=HTMLResponse)
async def render_stage() -> str:
return "<h1>Cipher</h1>"
@router.post("/verify")
async def verify_answer(request: Request, data: dict) -> dict:
if (data.get("answer") or "").strip() == "matrix":
next_level_index = CURRENT_LEVEL_INDEX + 1
next_slug = next_level_slug(CURRENT_LEVEL_INDEX)
player = getattr(request.state, "player", {}) or {}
user_id = player.get("uid") or str(uuid4())
token = generate_token(request=request, user_id=user_id, level=next_level_index)
return {"status": "success", "next_url": f"/{next_slug}" if next_slug else None, "token": token}
return {"status": "wrong"}4. (可选)添加静态页面
如果你更喜欢把页面放在静态资源里:
levels/cipher/static/index.html框架会把 static/ 自动挂到 /{slug}(例如 /cipher)。你可以在 index.html 里调用 /api/cipher/verify。
5. 运行与验证
启动:
bash
uvicorn main:app --reload验证路径:
- 直接访问页面:
GET /cipher(如果存在static/)或GET /api/cipher/(如果你用 router 返回 HTML) - 校验答案:
POST /api/cipher/verify(成功应返回token与next_url)
常见坑
- “明明写了关卡但访问 404”:检查
config/map.json是否有该条目且enabled: true,以及slug/module拼写一致。 - “下一关跳错/门禁不一致”:顺序以
config/map.json数组顺序为准,不要依赖id。 - “Lv1+ 请求被拦截”:确认请求携带
Authorization: Bearer <token>,且 token 的lvl≥ 当前关卡索引。