feat: 增加工作流编排临时草稿缓存
- 按当前标签页缓存并静默恢复未保存的工作流编排 - 优化试运行抽屉层级、窄屏宽度与长结果滚动 - 覆盖版本变化、缓存异常与保存竞态场景
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, onMounted, onUnmounted, ref} from 'vue';
|
||||
import {
|
||||
computed,
|
||||
onBeforeUnmount,
|
||||
onDeactivated,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
ref,
|
||||
} from 'vue';
|
||||
import {useRoute} from 'vue-router';
|
||||
|
||||
import {usePreferences} from '@easyflow/preferences';
|
||||
@@ -23,6 +30,12 @@ import ExecResult from '#/views/ai/workflow/components/ExecResult.vue';
|
||||
import SingleRun from '#/views/ai/workflow/components/SingleRun.vue';
|
||||
import WorkflowForm from '#/views/ai/workflow/components/WorkflowForm.vue';
|
||||
import WorkflowSteps from '#/views/ai/workflow/components/WorkflowSteps.vue';
|
||||
import {
|
||||
clearWorkflowDraft,
|
||||
createWorkflowContentSignature,
|
||||
readWorkflowDraft,
|
||||
writeWorkflowDraft,
|
||||
} from '#/views/ai/workflow/workflowDraftCache';
|
||||
|
||||
import {getCustomNode} from './customNode/index';
|
||||
import nodeNames from './customNode/nodeNames';
|
||||
@@ -48,12 +61,18 @@ onMounted(async () => {
|
||||
]);
|
||||
showTinyFlow.value = true;
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
captureCurrentWorkflowDraft();
|
||||
});
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', handleKeydown);
|
||||
if (focusPulseTimer) {
|
||||
clearTimeout(focusPulseTimer);
|
||||
}
|
||||
});
|
||||
onDeactivated(() => {
|
||||
captureCurrentWorkflowDraft();
|
||||
});
|
||||
// variables
|
||||
const tinyflowRef = ref<InstanceType<typeof Tinyflow> | null>(null);
|
||||
const workflowId = ref(route.query.id);
|
||||
@@ -69,6 +88,12 @@ const codeEngineList = ref<any[]>([
|
||||
available: true,
|
||||
},
|
||||
]);
|
||||
const WORKFLOW_DRAFT_WRITE_DELAY = 320;
|
||||
let draftWriteTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
let pendingDraftContent: any = null;
|
||||
let lastObservedWorkflowContent: any = null;
|
||||
let serverContentSignature = '';
|
||||
let draftCacheWarningShown = false;
|
||||
|
||||
function escapeHtmlAttr(value?: string) {
|
||||
return String(value || '')
|
||||
@@ -343,12 +368,16 @@ async function handleSave(showMsg: boolean = false): Promise<boolean> {
|
||||
saveLoading.value = true;
|
||||
try {
|
||||
const content = normalizeWorkflowStartNodes(tinyflowRef.value?.getData());
|
||||
const savedContentSignature = createWorkflowContentSignature(content);
|
||||
const res = await api.post('/api/v1/workflow/update', {
|
||||
id: workflowId.value,
|
||||
content,
|
||||
});
|
||||
if (res.errorCode === 0 && showMsg) {
|
||||
ElMessage.success(res.message);
|
||||
if (res.errorCode === 0) {
|
||||
reconcileWorkflowDraftAfterSave(savedContentSignature);
|
||||
if (showMsg) {
|
||||
ElMessage.success(res.message);
|
||||
}
|
||||
}
|
||||
return res.errorCode === 0;
|
||||
} catch {
|
||||
@@ -364,13 +393,101 @@ async function getWorkflowInfo(workflowId: any, syncFlowData: boolean = true) {
|
||||
const parsedContent = workflowInfo.value.content
|
||||
? JSON.parse(workflowInfo.value.content)
|
||||
: {};
|
||||
tinyFlowData.value = isWorkflowDataEmpty(parsedContent)
|
||||
const serverContent = isWorkflowDataEmpty(parsedContent)
|
||||
? createInitialWorkflowData()
|
||||
: normalizeWorkflowStartNodes(parsedContent);
|
||||
serverContentSignature = createWorkflowContentSignature(serverContent);
|
||||
const draft = readWorkflowDraft(workflowId, serverContent);
|
||||
tinyFlowData.value = draft
|
||||
? normalizeWorkflowStartNodes(draft.content as Record<string, any>)
|
||||
: serverContent;
|
||||
lastObservedWorkflowContent = tinyFlowData.value;
|
||||
}
|
||||
syncNavTitle(workflowInfo.value?.title || '');
|
||||
});
|
||||
}
|
||||
|
||||
function persistPendingWorkflowDraft() {
|
||||
if (!pendingDraftContent || !workflowId.value || !serverContentSignature) {
|
||||
draftWriteTimer = undefined;
|
||||
return;
|
||||
}
|
||||
const content = pendingDraftContent;
|
||||
pendingDraftContent = null;
|
||||
draftWriteTimer = undefined;
|
||||
const persisted = writeWorkflowDraft({
|
||||
baseContentSignature: serverContentSignature,
|
||||
content,
|
||||
workflowId: workflowId.value,
|
||||
});
|
||||
if (!persisted && !draftCacheWarningShown) {
|
||||
draftCacheWarningShown = true;
|
||||
ElMessage.warning($t('aiWorkflow.draftCacheFailed'));
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleWorkflowDraft(content: any) {
|
||||
lastObservedWorkflowContent = normalizeWorkflowStartNodes(content);
|
||||
pendingDraftContent = lastObservedWorkflowContent;
|
||||
if (draftWriteTimer) {
|
||||
clearTimeout(draftWriteTimer);
|
||||
}
|
||||
draftWriteTimer = setTimeout(
|
||||
persistPendingWorkflowDraft,
|
||||
WORKFLOW_DRAFT_WRITE_DELAY,
|
||||
);
|
||||
}
|
||||
|
||||
function flushWorkflowDraft() {
|
||||
if (draftWriteTimer) {
|
||||
clearTimeout(draftWriteTimer);
|
||||
}
|
||||
if (pendingDraftContent) {
|
||||
persistPendingWorkflowDraft();
|
||||
}
|
||||
}
|
||||
|
||||
function captureCurrentWorkflowDraft() {
|
||||
const content = tinyflowRef.value?.getData();
|
||||
if (content) {
|
||||
lastObservedWorkflowContent = normalizeWorkflowStartNodes(content);
|
||||
pendingDraftContent = lastObservedWorkflowContent;
|
||||
}
|
||||
flushWorkflowDraft();
|
||||
}
|
||||
|
||||
function clearPendingWorkflowDraft() {
|
||||
if (draftWriteTimer) {
|
||||
clearTimeout(draftWriteTimer);
|
||||
}
|
||||
draftWriteTimer = undefined;
|
||||
pendingDraftContent = null;
|
||||
clearWorkflowDraft(workflowId.value);
|
||||
}
|
||||
|
||||
function reconcileWorkflowDraftAfterSave(savedContentSignature: string) {
|
||||
serverContentSignature = savedContentSignature;
|
||||
const currentContent =
|
||||
tinyflowRef.value?.getData() || lastObservedWorkflowContent;
|
||||
if (!currentContent) {
|
||||
clearPendingWorkflowDraft();
|
||||
return;
|
||||
}
|
||||
const normalizedContent = normalizeWorkflowStartNodes(currentContent);
|
||||
lastObservedWorkflowContent = normalizedContent;
|
||||
if (
|
||||
createWorkflowContentSignature(normalizedContent) === savedContentSignature
|
||||
) {
|
||||
clearPendingWorkflowDraft();
|
||||
return;
|
||||
}
|
||||
if (draftWriteTimer) {
|
||||
clearTimeout(draftWriteTimer);
|
||||
}
|
||||
draftWriteTimer = undefined;
|
||||
pendingDraftContent = normalizedContent;
|
||||
persistPendingWorkflowDraft();
|
||||
}
|
||||
async function getLlmList() {
|
||||
return api.get('/api/v1/model/list').then((res) => {
|
||||
llmList.value = res.data;
|
||||
@@ -652,12 +769,18 @@ function onAsyncExecute(info: any) {
|
||||
<ElDrawer
|
||||
v-model="singleRunVisible"
|
||||
:title="singleNode?.data?.title"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
size="600px"
|
||||
size="min(600px, 94vw)"
|
||||
>
|
||||
<SingleRun :node="singleNode" :workflow-id="workflowId" />
|
||||
</ElDrawer>
|
||||
<ElDrawer v-model="drawerVisible" :title="$t('button.run')" size="600px">
|
||||
<ElDrawer
|
||||
v-model="drawerVisible"
|
||||
:title="$t('button.run')"
|
||||
append-to-body
|
||||
size="min(600px, 94vw)"
|
||||
>
|
||||
<div class="mb-2.5 font-semibold">{{ $t('aiWorkflow.params') }}:</div>
|
||||
<WorkflowForm
|
||||
ref="workflowForm"
|
||||
@@ -733,6 +856,7 @@ function onAsyncExecute(info: any) {
|
||||
:theme="isDark ? 'dark' : 'light'"
|
||||
:provider="provider"
|
||||
:custom-nodes="customNode"
|
||||
:on-data-change="scheduleWorkflowDraft"
|
||||
:on-node-execute="runIndependently"
|
||||
:on-run-test="runWorkflow"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user