From a5d9f0bfee086a3ab71f3d58bc6055bec5d394e7 Mon Sep 17 00:00:00 2001 From: yu <191972707@qq.com> Date: Wed, 28 Oct 2026 17:38:41 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=B6=88=E9=99=A4=E4=BF=9D=E5=AD=98?= =?UTF-8?q?=E5=B9=B6=E5=8F=91=E7=AB=9E=E6=80=81=E2=80=94=E2=80=94=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E9=94=81=E9=98=B2=E9=87=8D=E5=A4=8D=E4=BF=9D=E5=AD=98?= =?UTF-8?q?=EF=BC=8C=E5=86=B2=E7=AA=81=E5=88=A4=E5=AE=9A=E5=85=88=E6=AF=94?= =?UTF-8?q?=E5=AF=B9=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/index.ts | 23 ++++++++++++++++------- src/renderer/src/pages/EditorPage.tsx | 11 ++++++++--- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index 00f71f1..bfa11e6 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -129,14 +129,23 @@ function registerIpc(): void { }); ipcMain.handle("flows:get", (_event, id: string) => getFlow(id)); ipcMain.handle("flows:save", (_event, flow: FlowDefinition, opts?: FlowSaveOptions): FlowSaveResult => { - if (!opts?.force && opts?.expectedUpdatedAt) { - const disk = getFlow(flow.id); - if (disk && disk.meta?.updatedAt && disk.meta.updatedAt !== opts.expectedUpdatedAt) { - return { conflict: true, diskFlow: disk }; + if (!opts?.force && opts?.expectedUpdatedAt) { + let disk: FlowDefinition | null = null; + try { + disk = getFlow(flow.id); + } catch { + disk = null; // 磁盘文件不存在或损坏时视为全新保存 + } + if (disk && disk.meta?.updatedAt && disk.meta.updatedAt !== opts.expectedUpdatedAt) { + // 内容一致(仅 updatedAt 差异,如并发/重复保存)不视为冲突 + const normalize = (item: FlowDefinition) => JSON.stringify({ ...item, meta: undefined }); + if (normalize(disk) !== normalize(flow)) { + return { conflict: true, diskFlow: disk }; + } + } } - } - return { conflict: false, flow: saveFlow(flow) }; -}); + return { conflict: false, flow: saveFlow(flow) }; + }); ipcMain.handle("flows:delete", (_event, id: string) => { deleteFlow(id); return true; diff --git a/src/renderer/src/pages/EditorPage.tsx b/src/renderer/src/pages/EditorPage.tsx index f3b2c8e..d1373f6 100644 --- a/src/renderer/src/pages/EditorPage.tsx +++ b/src/renderer/src/pages/EditorPage.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { ArrowLeft, Check, CircleAlert, CirclePlay, Lightbulb, Loader2, Redo2, Save, Square, Undo2, X } from "lucide-react"; import type { Catalog } from "@shared/types"; import { useEditorStore } from "../store/editorStore"; @@ -34,6 +34,9 @@ export function EditorPage({ catalog, onToast, onBack }: EditorPageProps) { const startRun = useRunStore((state) => state.start); const stopRun = useRunStore((state) => state.stop); const [saving, setSaving] = useState(false); + // 同步锁:防止自动保存定时器与按钮点击(或双击)在同一帧内触发两次并发保存, + // 并发保存会让后到者误判磁盘版本冲突(saving state 异步更新拦不住同一帧的第二次调用) + const savingRef = useRef(false); const [saveConflict, setSaveConflict] = useState(false); const [savedAt, setSavedAt] = useState(null); const [validationIssues, setValidationIssues] = useState(null); @@ -57,7 +60,8 @@ export function EditorPage({ catalog, onToast, onBack }: EditorPageProps) { const save = useCallback(async (opts?: { interactive?: boolean }) => { const current = useEditorStore.getState().flow; - if (!current || saving) return null; + if (!current || savingRef.current) return null; + savingRef.current = true; setSaving(true); try { const result = await window.rpa.flows.save(current, { @@ -99,9 +103,10 @@ export function EditorPage({ catalog, onToast, onBack }: EditorPageProps) { onToast(error instanceof Error ? error.message : String(error), "error"); return null; } finally { + savingRef.current = false; setSaving(false); } - }, [loadFlow, markSaved, onToast, saving, syncSavedFlow]); + }, [loadFlow, markSaved, onToast, syncSavedFlow]); useEffect(() => { if (!dirty || saveConflict) return;