from __future__ import annotations from pathlib import Path from fastapi import APIRouter, HTTPException, Request from pydantic import BaseModel, Field from app.presets import PRESETS from app.services.i18n import DEFAULT_LOCALE, locale_from_request router = APIRouter(prefix="/api") CUSTOM_SIMULATION_DIR = Path(__file__).resolve().parents[3] / "custom_simulation" class CustomPresetPayload(BaseModel): title: str = Field(min_length=1, max_length=160) content: str = Field(min_length=1) def _localize(value, locale: str): """Recursively unwrap {en, zh} dicts to the requested locale. Falls back to the default locale, then to the first available value. Strings (and other scalars) are returned as-is. """ if isinstance(value, dict): # Heuristic: dicts with only "en"/"zh" keys are translation maps. keys = set(value.keys()) if keys <= {"en", "zh"}: if locale in value: return value[locale] if DEFAULT_LOCALE in value: return value[DEFAULT_LOCALE] return next(iter(value.values())) return {k: _localize(v, locale) for k, v in value.items()} if isinstance(value, list): return [_localize(v, locale) for v in value] return value @router.get("/presets") async def get_presets(request: Request): locale = locale_from_request(request) return {"presets": _localize(PRESETS, locale)} def _custom_presets(): CUSTOM_SIMULATION_DIR.mkdir(parents=True, exist_ok=True) presets = [] for path in sorted(CUSTOM_SIMULATION_DIR.glob("*.txt"), key=lambda p: p.name.lower()): try: content = path.read_text(encoding="utf-8") except (OSError, UnicodeDecodeError): continue presets.append({ "id": f"custom:{path.name}", "name": path.stem, "rules": content, "custom": True, "filename": path.name, }) return presets @router.get("/custom-presets") async def get_custom_presets(): """Load user-authored scenario files from the project custom_simulation folder.""" return {"presets": _custom_presets()} @router.post("/custom-presets") async def create_custom_preset(payload: CustomPresetPayload): """Save a custom scenario, suffixing duplicate titles with _1, _2, ... .""" title = " ".join(payload.title.strip().split()) # Keep unicode letters (including Chinese) while preventing path traversal. title = title.replace("/", "/").replace("\\", "\") if not title or title in {".", ".."}: raise HTTPException(status_code=400, detail="Title is required") CUSTOM_SIMULATION_DIR.mkdir(parents=True, exist_ok=True) candidate = CUSTOM_SIMULATION_DIR / f"{title}.txt" index = 1 while candidate.exists(): candidate = CUSTOM_SIMULATION_DIR / f"{title}_{index}.txt" index += 1 try: candidate.write_text(payload.content, encoding="utf-8") except OSError as exc: raise HTTPException(status_code=500, detail=f"Unable to save preset: {exc}") from exc return { "preset": { "id": f"custom:{candidate.name}", "name": candidate.stem, "rules": payload.content, "custom": True, "filename": candidate.name, } }