Files
90CNServer/gmtool/main.py
T
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

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")