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;