服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
413 lines
14 KiB
Python
413 lines
14 KiB
Python
# -*- 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")
|