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