DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

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

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

No files matched your search

+125
View File
@@ -0,0 +1,125 @@
import { nextTick } from "vue";
import { setMCPActive } from "./mcpActivity";
import { Events } from "@wailsio/runtime";
import { MCPService } from "../bindings/pvfine/services";
import { useEditorStore } from "./stores/editor";
import { useArchiveStore } from "./stores/archive";
import { useScriptStore } from "./stores/script";
import { useImportStore } from "./stores/import";
import { useExportStore } from "./stores/export";
import { useBatchStore } from "./stores/batch";
import { useVersionStore } from "./stores/version";
/** Every window acknowledges a preparation barrier before an AI task starts. */
export function installMCPBridge(scriptWindow: boolean): void {
const id = scriptWindow ? "script-workspace" : "main";
const editor = scriptWindow ? null : useEditorStore();
const archive = useArchiveStore();
const script = useScriptStore();
const imports = scriptWindow ? null : useImportStore();
const exports = scriptWindow ? null : useExportStore();
const batch = scriptWindow ? null : useBatchStore();
const version = scriptWindow ? null : useVersionStore();
const isBusy = () => !!(editor?.saving || editor?.guiApplying || editor?.opening || archive.loading || script.running || script.applying || imports?.running || exports?.running || exports?.preparing || batch?.loading || batch?.applying || version?.committing);
let operation = "";
let barrierGeneration = 0;
let pendingSync = Promise.resolve();
let syncFailed = false;
const root = document.getElementById("app")!;
const banner = document.createElement("div");
banner.setAttribute("role", "status");
banner.style.cssText = "position:fixed;bottom:18px;left:50%;transform:translateX(-50%);z-index:99999;padding:10px 18px;border-radius:8px;background:#25334a;color:white;font-size:13px;display:none;pointer-events:none";
const label = document.createElement("span");
label.textContent = "AI 正在操作当前 PVF… ";
banner.appendChild(label);
let noticeTimer: ReturnType<typeof setTimeout> | undefined;
const cancel = document.createElement("button");
cancel.textContent = "取消脚本";
cancel.style.cssText = "margin-left:12px;cursor:pointer;pointer-events:auto";
cancel.addEventListener("click", () => { void MCPService.CancelActiveScript(); });
banner.appendChild(cancel);
document.body.appendChild(banner);
const release = () => {
barrierGeneration++;
operation = "";
setMCPActive(false);
root.inert = false;
banner.style.display = "none";
};
// Block app-level shortcuts as well as native editing while the barrier holds.
const blockInput = (event: Event) => {
if (operation && !banner.contains(event.target as Node)) { event.preventDefault(); event.stopImmediatePropagation(); }
};
const inputEvents = ["keydown", "pointerdown", "click", "beforeinput", "drop"];
for (const name of inputEvents) window.addEventListener(name, blockInput, true);
const offPrepare = Events.On("mcp:prepare", async (event: any) => {
const data = event?.data ?? event;
if (!data?.operationId) return;
clearTimeout(noticeTimer);
label.textContent = "AI 正在操作当前 PVF… ";
barrierGeneration++;
operation = data.operationId;
setMCPActive(true);
cancel.style.display = data.kind === "script_run" ? "inline" : "none";
root.inert = true;
banner.style.display = "block";
await nextTick();
await pendingSync;
let dirty = syncFailed;
try {
if (editor) dirty = dirty || editor.dirtyCount > 0 || editor.archiveDiffDrafts().length > 0;
} catch { dirty = true; }
const busy = isBusy();
await MCPService.Acknowledge(id, operation, dirty, busy);
});
const offWorkspace = Events.On("mcp:workspace-changed", (event: any) => {
const data = event?.data ?? event;
if (!editor) return;
pendingSync = pendingSync.then(async () => {
if (data.kind === "archive_open") editor.closeAllTabs();
else if (data.kind !== "archive_close") await editor.syncMCPWorkspace();
syncFailed = false;
}).catch(() => { syncFailed = true; });
});
const offRelease = Events.On("mcp:released", async (event: any) => {
const data = event?.data ?? event;
await pendingSync;
if (operation === data?.operationId) release();
});
const offFinished = Events.On("mcp:task-finished", async (event: any) => {
await pendingSync;
const data = event?.data ?? event;
const names: Record<string, string> = { script_run: "脚本运行", script_apply: "应用脚本", import_prepare: "导入预览", import_apply: "导入", export_prepare: "导出预览", export_execute: "导出", archive_open: "打开归档", archive_close: "关闭归档", archive_save: "保存", archive_save_as: "生成副本" };
if (operation) return;
label.textContent = data.status === "failed" ? `AI ${names[data.kind] ?? "任务"}失败:${data.error ?? "请查看任务结果"}` : `AI ${names[data.kind] ?? "任务"}完成${data.outputPath ? `:${data.outputPath}` : ""}`;
cancel.style.display = "none";
banner.style.display = "block";
noticeTimer = setTimeout(() => { if (!operation) banner.style.display = "none"; }, 6000);
});
// Refreshing the window recovers retained AI script previews without touching source.
const heartbeat = async () => {
const generation = barrierGeneration;
try {
let dirty = false;
try { dirty = !!editor && (editor.dirtyCount > 0 || editor.archiveDiffDrafts().length > 0); } catch { dirty = true; }
await MCPService.ReportWindow(id, dirty, isBusy());
const state = await MCPService.WindowState();
if (generation !== barrierGeneration) return;
if (state?.operationId) {
operation = state.operationId;
setMCPActive(true);
root.inert = true;
banner.style.display = "block";
cancel.style.display = state.kind === "script_run" ? "inline" : "none";
} else if (operation) release();
} catch { /* A closing runtime will disconnect naturally. */ }
};
void heartbeat();
const timer = window.setInterval(() => void heartbeat(), 3000);
window.addEventListener("beforeunload", () => {
window.clearInterval(timer);
offPrepare(); offRelease(); offFinished(); offWorkspace(); clearTimeout(noticeTimer); release(); banner.remove();
for (const name of inputEvents) window.removeEventListener(name, blockInput, true);
void MCPService.UnregisterWindow(id);
}, { once: true });
}