# -*- coding: utf-8 -*- """DNF90 GM 工具 —— 后端服务。 基于 FastAPI,作为浏览器前端与服务端本地 GM API 之间的代理, 同时提供服务端自动发现、状态轮询、以及静态资源托管。 """ import os from fastapi import FastAPI, HTTPException, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, JSONResponse from fastapi.staticfiles import StaticFiles from gm_client import get_client, reconnect, GMClient BASE_DIR = os.path.dirname(os.path.abspath(__file__)) STATIC_DIR = os.path.join(BASE_DIR, "static") app = FastAPI(title="DNF90 GM 工具", version="2.0.0") app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) def _ok(data): return {"ok": True, "data": data} def _unwrap(result): """把 gm_client 返回的 {'ok':.., 'data':..} 统一为 HTTP 响应。""" if not isinstance(result, dict): return JSONResponse({"ok": False, "error": "未知响应"}, status_code=502) if result.get("ok"): return JSONResponse({"ok": True, "data": result.get("data")}) err = result.get("error") or "操作失败" return JSONResponse({"ok": False, "error": err}, status_code=400) # ------------------------------------------------------------------ 系统 @app.get("/api/health") def health(): client = get_client() return {"ok": True, "base_url": client.base_url, "token_ok": bool(client.token)} @app.post("/api/connect") def connect(payload: dict): host = payload.get("host", "127.0.0.1") port = int(payload.get("port", 18111)) token = payload.get("token") or None client = reconnect(host, port, token) res = client.status() if not res.get("ok"): return JSONResponse({"ok": False, "error": "无法连接服务端:%s" % res.get("error")}, status_code=400) return {"ok": True, "base_url": client.base_url, "status": res.get("data")} @app.get("/api/discover") def discover(): base_url, token = GMClient.discover() if base_url: # 成功发现则建立连接 host = base_url.split("://")[1].split(":")[0] port = int(base_url.rsplit(":", 1)[1]) reconnect(host, port, token) return {"ok": True, "base_url": base_url, "token": token} return {"ok": False, "error": "未发现运行中的服务端"} # ------------------------------------------------------------------ 状态 / 频道 @app.get("/api/status") def status(): return _unwrap(get_client().status()) @app.get("/api/channels") def channels(): return _unwrap(get_client().channels()) @app.post("/api/channels/reload") def reload_channels(): return _unwrap(get_client().reload_channels()) @app.get("/api/gameplay") def gameplay(): return _unwrap(get_client().gameplay()) # ------------------------------------------------------------------ 账号 @app.get("/api/accounts") def accounts(q: str = "", limit: int = 50): return _unwrap(get_client().list_accounts(q, limit)) @app.get("/api/accounts/{account_id}") def account(account_id: str): return _unwrap(get_client().account(account_id)) @app.get("/api/accounts/{account_id}/characters") def account_characters(account_id: str): return _unwrap(get_client().account_characters(account_id)) @app.post("/api/accounts/{account_id}/value") def set_account_value(account_id: str, payload: dict): return _unwrap(get_client().set_account_value( account_id, payload.get("field"), payload.get("value"))) # ------------------------------------------------------------------ 角色 @app.get("/api/characters") def characters(account_id: str = "", character_name: str = "", username: str = ""): return _unwrap(get_client().characters(account_id, character_name, username)) @app.get("/api/characters/{character_id}") def character(character_id: str): return _unwrap(get_client().character(character_id)) @app.get("/api/characters/{character_id}/detail") def character_detail(character_id: str): return _unwrap(get_client().character_detail(character_id)) @app.post("/api/characters/{character_id}/gold") def set_gold(character_id: str, payload: dict): return _unwrap(get_client().set_gold(character_id, payload.get("value", 0))) @app.post("/api/characters/{character_id}/level") def set_level(character_id: str, payload: dict): return _unwrap(get_client().set_level(character_id, payload.get("level", 1))) @app.post("/api/characters/{character_id}/skill-points") def set_skill(character_id: str, payload: dict): return _unwrap(get_client().set_skill_points( character_id, payload.get("bonus_sp", 0), payload.get("bonus_tp", 0))) @app.post("/api/characters/{character_id}/career") def set_career(character_id: str, payload: dict): return _unwrap(get_client().set_career( character_id, payload.get("job", 0), payload.get("first", 0), payload.get("awakening", 0))) # ------------------------------------------------------------------ 邮件 @app.get("/api/characters/{character_id}/mailbox") def mailbox(character_id: str): return _unwrap(get_client().mailbox(character_id)) @app.post("/api/characters/{character_id}/mailbox/delete") def delete_mail(character_id: str, payload: dict): return _unwrap(get_client().delete_mail( character_id, payload.get("mail_ids", []), payload.get("force", False))) @app.post("/api/characters/{character_id}/mail") def send_mail(character_id: str, payload: dict): return _unwrap(get_client().send_mail( character_id, payload.get("item_id", 0), payload.get("count", 1), payload.get("title", ""), payload.get("body", ""), expire_days=payload.get("expire_days", 7), reinforce=payload.get("reinforce", 0), amplify_type=payload.get("amplify_type", 0), equipment_quality=payload.get("equipment_quality", ""), forging=payload.get("forging", 0), emblem_item_ids=payload.get("emblem_item_ids", []), enchant_bead_item_id=payload.get("enchant_bead_item_id", 0), )) @app.post("/api/system-mail") def system_mail(payload: dict): return _unwrap(get_client().system_mail( payload.get("character_name", ""), payload.get("title", ""), payload.get("body", ""), gold=payload.get("gold", 0), attachments=payload.get("attachments", []), )) # ------------------------------------------------------------------ 道具 @app.get("/api/items/search") def search_items(q: str = "", limit: int = 30): return _unwrap(get_client().search_items(q, limit)) @app.get("/api/items/page") def item_page(q: str = "", kind: str = "", tag: str = "", segment: str = "", group: str = "", job: str = "", min_level: int = 0, max_level: int = 0, rarity: int = -1, expiration: str = "", limit: int = 100, offset: int = 0): return _unwrap(get_client().item_page( q=q, kind=kind, tag=tag, segment=segment, group=group, job=job, min_level=min_level, max_level=max_level, rarity=rarity, expiration=expiration, limit=limit, offset=offset)) @app.get("/api/items/{item_id}/detail") def item_detail(item_id: int): return _unwrap(get_client().item_detail(item_id)) @app.get("/api/items/categories") def item_categories(): return _unwrap(get_client().item_categories()) @app.get("/api/items/{item_id}/mail-options") def item_mail_options(item_id: int, mode: str = "", q: str = "", rarity: int = -1, limit: int = 40, offset: int = 0): return _unwrap(get_client().item_mail_options(item_id, mode, q, rarity, limit, offset)) @app.post("/api/characters/{character_id}/grant") def grant_item(character_id: str, payload: dict): return _unwrap(get_client().grant_item( character_id, payload.get("item_id", 0), payload.get("count", 1))) # ------------------------------------------------------------------ 背包 / 装备 @app.get("/api/characters/{character_id}/inventory") def inventory(character_id: str): return _unwrap(get_client().inventory(character_id)) @app.post("/api/characters/{character_id}/inventory/delete") def delete_inventory_item(character_id: str, payload: dict): return _unwrap(get_client().delete_inventory_item( character_id, payload.get("container", ""), payload.get("key", ""), payload.get("count", 1))) @app.post("/api/characters/{character_id}/inventory/clear") def clear_inventory(character_id: str, payload: dict): return _unwrap(get_client().clear_inventory(character_id, payload.get("container", ""))) @app.get("/api/characters/{character_id}/equipment-slots") def equipment_slots(character_id: str): return _unwrap(get_client().extra_equipment_slots(character_id)) @app.post("/api/characters/{character_id}/equipment-slots/unlock") def unlock_equipment_slots(character_id: str, payload: dict): return _unwrap(get_client().unlock_equipment_slots(character_id, payload.get("slots", []))) @app.post("/api/characters/{character_id}/equipment/unequip") def unequip(character_id: str, payload: dict): return _unwrap(get_client().unequip(character_id, payload.get("key", ""))) @app.post("/api/characters/{character_id}/equipment/delete") def delete_equipment(character_id: str, payload: dict): return _unwrap(get_client().delete_equipment(character_id, payload.get("key", ""))) # ------------------------------------------------------------------ 任务 @app.get("/api/characters/{character_id}/quests") def quests(character_id: str): return _unwrap(get_client().quests(character_id)) @app.get("/api/characters/{character_id}/quests/search") def search_quests(character_id: str, q: str = "", limit: int = 40): return _unwrap(get_client().search_quests(character_id, q, limit)) @app.get("/api/characters/{character_id}/quests/{quest_id}/detail") def quest_detail(character_id: str, quest_id: int): return _unwrap(get_client().quest_detail(character_id, quest_id)) @app.post("/api/characters/{character_id}/quests/{quest_id}/{action}") def quest_action(character_id: str, quest_id: int, action: str): allowed = {"accept", "ready", "give-up", "complete", "uncomplete"} if action not in allowed: raise HTTPException(400, "不支持的操作") return _unwrap(get_client().quest_action(character_id, quest_id, action)) @app.post("/api/characters/{character_id}/quests/complete-batch") def quest_batch(character_id: str, payload: dict): return _unwrap(get_client().quest_complete_batch( character_id, payload.get("quest_ids", []))) @app.post("/api/characters/{character_id}/quest-resync") def quest_resync(character_id: str): return _unwrap(get_client().quest_resync(character_id)) # ------------------------------------------------------------------ 副本 @app.get("/api/characters/{character_id}/dungeons") def dungeons(character_id: str): return _unwrap(get_client().dungeons(character_id)) @app.post("/api/characters/{character_id}/dungeons/unlock-difficulties") def unlock_difficulties(character_id: str): return _unwrap(get_client().unlock_dungeon_difficulties(character_id)) @app.post("/api/characters/{character_id}/dungeons/unlock-hell") def unlock_hell(character_id: str): return _unwrap(get_client().unlock_hell(character_id)) @app.get("/api/characters/{character_id}/dungeon-control") def dungeon_control(character_id: str): return _unwrap(get_client().dungeon_control(character_id)) @app.post("/api/characters/{character_id}/dungeon-return") def dungeon_return(character_id: str): return _unwrap(get_client().dungeon_return_town(character_id)) # ------------------------------------------------------------------ 钱包 @app.post("/api/characters/{character_id}/wallet/query") def wallet_query(character_id: str): return _unwrap(get_client().wallet_query(character_id)) @app.post("/api/characters/{character_id}/wallet/update") def wallet_update(character_id: str, payload: dict): return _unwrap(get_client().wallet_update( character_id, payload.get("field"), payload.get("value"))) @app.post("/api/characters/{character_id}/revive-coin") def revive_coin(character_id: str, payload: dict): return _unwrap(get_client().revive_coin(character_id, payload.get("delta", 0))) @app.get("/api/black-diamond") def black_diamond(id: str = "", name: str = ""): return _unwrap(get_client().black_diamond_status(id, name)) @app.post("/api/black-diamond/activate") def activate_black_diamond(payload: dict): return _unwrap(get_client().activate_black_diamond( payload.get("character_id", ""), payload.get("days", 30))) # ------------------------------------------------------------------ 玩法 @app.post("/api/gameplay/hell-drop-rate") def hell_drop_rate(payload: dict): return _unwrap(get_client().set_hell_drop_rate(payload.get("percent", 0))) @app.post("/api/gameplay/epic-potion-multiplier") def epic_potion(payload: dict): return _unwrap(get_client().set_epic_potion_multiplier(payload.get("percent", 0))) # ------------------------------------------------------------------ 其他 @app.post("/api/kick") def kick(payload: dict): return _unwrap(get_client().kick_sessions( payload.get("ban_type", "account"), payload.get("ban_value", ""))) @app.post("/api/clone") def clone(payload: dict): return _unwrap(get_client().clone_character( payload.get("source_character_id", 0), payload.get("name", ""))) @app.get("/api/records/meta") def record_meta(): return _unwrap(get_client().record_meta()) @app.post("/api/records/query") def record_query(payload: dict): return _unwrap(get_client().query_records(**payload)) # ------------------------------------------------------------------ 静态资源 @app.get("/") def index(): return FileResponse(os.path.join(STATIC_DIR, "index.html")) app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") if __name__ == "__main__": import uvicorn print("=" * 60) print(" DNF90 GM 工具后端已启动") print(" 访问: http://127.0.0.1:9527") print("=" * 60) uvicorn.run(app, host="127.0.0.1", port=9527, log_level="info")