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