fix: 清理前端异步请求生命周期
- 页面卸载和弹窗关闭时终止 SSE - 为版本检查增加超时、中止与重复请求保护
This commit is contained in:
@@ -578,6 +578,9 @@ watch(
|
||||
{ flush: 'post' },
|
||||
);
|
||||
onBeforeUnmount(() => {
|
||||
if (sending.value) {
|
||||
stopSse();
|
||||
}
|
||||
if (bubbleListScrollElement.value) {
|
||||
bubbleListScrollElement.value.removeEventListener(
|
||||
'scroll',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { onBeforeUnmount, ref, watch } from 'vue';
|
||||
|
||||
import { EasyFlowPanelModal } from '@easyflow/common-ui';
|
||||
import { $t } from '@easyflow/locales';
|
||||
@@ -18,6 +18,16 @@ const rules = {
|
||||
};
|
||||
const loading = ref(false);
|
||||
|
||||
const stopStreaming = () => {
|
||||
if (!loading.value) return;
|
||||
sseClient.abort();
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
loading.value = true;
|
||||
const data = {
|
||||
@@ -40,12 +50,28 @@ const handleSubmit = async () => {
|
||||
const delta = sseData.payload?.delta;
|
||||
formData.value.prompt += delta;
|
||||
},
|
||||
onError(error) {
|
||||
loading.value = false;
|
||||
console.error('提示词优化流式请求失败', error);
|
||||
},
|
||||
onFinished() {
|
||||
loading.value = false;
|
||||
},
|
||||
});
|
||||
};
|
||||
const handleReplace = () => {
|
||||
emit('success', formData.value.prompt);
|
||||
dialogVisible.value = false;
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
watch(dialogVisible, (visible) => {
|
||||
if (!visible) {
|
||||
stopStreaming();
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(stopStreaming);
|
||||
|
||||
defineExpose({
|
||||
open(botId: string, systemPrompt: string) {
|
||||
formData.value = {
|
||||
@@ -80,7 +106,7 @@ defineExpose({
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="dialogVisible = false">
|
||||
<ElButton @click="closeDialog">
|
||||
{{ $t('button.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton type="primary" @click="handleReplace" v-if="!loading">
|
||||
|
||||
@@ -351,6 +351,9 @@ watch(
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (sending.value || activeStreamSessionId.value) {
|
||||
abortActiveStream();
|
||||
}
|
||||
window.removeEventListener('click', closeSessionContextMenu);
|
||||
window.removeEventListener('resize', closeSessionContextMenu);
|
||||
window.removeEventListener('scroll', closeSessionContextMenu, true);
|
||||
|
||||
@@ -21,6 +21,8 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
|
||||
let isCheckingUpdates = false;
|
||||
let resumeCheckFrame: number | undefined;
|
||||
let versionCheckController: AbortController | null = null;
|
||||
const VERSION_CHECK_TIMEOUT_MS = 10_000;
|
||||
const currentVersionTag = ref('');
|
||||
const lastVersionTag = ref('');
|
||||
const timer = ref<ReturnType<typeof setInterval>>();
|
||||
@@ -37,6 +39,12 @@ const [UpdateNoticeModal, modalApi] = useEasyFlowModal({
|
||||
});
|
||||
|
||||
async function getVersionTag() {
|
||||
const controller = new AbortController();
|
||||
versionCheckController = controller;
|
||||
const timeout = setTimeout(
|
||||
() => controller.abort(),
|
||||
VERSION_CHECK_TIMEOUT_MS,
|
||||
);
|
||||
try {
|
||||
if (
|
||||
location.hostname === 'localhost' ||
|
||||
@@ -48,14 +56,22 @@ async function getVersionTag() {
|
||||
cache: 'no-cache',
|
||||
method: 'HEAD',
|
||||
redirect: 'manual',
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
return (
|
||||
response.headers.get('etag') || response.headers.get('last-modified')
|
||||
);
|
||||
} catch {
|
||||
console.error('Failed to fetch version tag');
|
||||
} catch (error) {
|
||||
if (!controller.signal.aborted) {
|
||||
console.error('Failed to fetch version tag', error);
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
if (versionCheckController === controller) {
|
||||
versionCheckController = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,10 +88,21 @@ async function checkForUpdates() {
|
||||
}
|
||||
|
||||
if (lastVersionTag.value !== versionTag && versionTag) {
|
||||
clearInterval(timer.value);
|
||||
stop();
|
||||
handleNotice(versionTag);
|
||||
}
|
||||
}
|
||||
|
||||
async function runUpdateCheck() {
|
||||
if (isCheckingUpdates) return;
|
||||
isCheckingUpdates = true;
|
||||
try {
|
||||
await checkForUpdates();
|
||||
} finally {
|
||||
isCheckingUpdates = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleNotice(versionTag: string) {
|
||||
currentVersionTag.value = versionTag;
|
||||
modalApi.open();
|
||||
@@ -89,7 +116,7 @@ function start() {
|
||||
|
||||
// 每 checkUpdatesInterval(默认值为1) 分钟检查一次
|
||||
timer.value = setInterval(
|
||||
checkForUpdates,
|
||||
() => void runUpdateCheck(),
|
||||
props.checkUpdatesInterval * 60 * 1000,
|
||||
);
|
||||
}
|
||||
@@ -108,13 +135,11 @@ function scheduleResumeCheck() {
|
||||
resumeCheckFrame = requestAnimationFrame(() => {
|
||||
resumeCheckFrame = requestAnimationFrame(() => {
|
||||
resumeCheckFrame = undefined;
|
||||
if (document.hidden || isCheckingUpdates) {
|
||||
return;
|
||||
}
|
||||
isCheckingUpdates = true;
|
||||
checkForUpdates().finally(() => {
|
||||
isCheckingUpdates = false;
|
||||
if (document.hidden) return;
|
||||
runUpdateCheck().finally(() => {
|
||||
if (!document.hidden) {
|
||||
start();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -130,12 +155,13 @@ function handleVisibilitychange() {
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (timer.value === undefined) {
|
||||
return;
|
||||
}
|
||||
if (timer.value !== undefined) {
|
||||
clearInterval(timer.value);
|
||||
timer.value = undefined;
|
||||
}
|
||||
versionCheckController?.abort();
|
||||
versionCheckController = null;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
start();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { ChatTimeTimelineItem } from '@easyflow/types';
|
||||
|
||||
import { inject, ref } from 'vue';
|
||||
import { inject, onBeforeUnmount, ref } from 'vue';
|
||||
|
||||
import { ChatTimeTimelineBuilder, uuid } from '@easyflow/utils';
|
||||
|
||||
@@ -237,6 +237,12 @@ function normalizeSseRoundMeta(meta: any): SseRoundMeta {
|
||||
variantIndex,
|
||||
};
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (btnLoading.value) {
|
||||
stopSse();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -20,6 +20,8 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
});
|
||||
|
||||
let isCheckingUpdates = false;
|
||||
let versionCheckController: AbortController | null = null;
|
||||
const VERSION_CHECK_TIMEOUT_MS = 10_000;
|
||||
const currentVersionTag = ref('');
|
||||
const lastVersionTag = ref('');
|
||||
const timer = ref<ReturnType<typeof setInterval>>();
|
||||
@@ -36,6 +38,12 @@ const [UpdateNoticeModal, modalApi] = useEasyFlowModal({
|
||||
});
|
||||
|
||||
async function getVersionTag() {
|
||||
const controller = new AbortController();
|
||||
versionCheckController = controller;
|
||||
const timeout = setTimeout(
|
||||
() => controller.abort(),
|
||||
VERSION_CHECK_TIMEOUT_MS,
|
||||
);
|
||||
try {
|
||||
if (
|
||||
location.hostname === 'localhost' ||
|
||||
@@ -47,14 +55,22 @@ async function getVersionTag() {
|
||||
cache: 'no-cache',
|
||||
method: 'HEAD',
|
||||
redirect: 'manual',
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
return (
|
||||
response.headers.get('etag') || response.headers.get('last-modified')
|
||||
);
|
||||
} catch {
|
||||
console.error('Failed to fetch version tag');
|
||||
} catch (error) {
|
||||
if (!controller.signal.aborted) {
|
||||
console.error('Failed to fetch version tag', error);
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
if (versionCheckController === controller) {
|
||||
versionCheckController = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,23 +87,35 @@ async function checkForUpdates() {
|
||||
}
|
||||
|
||||
if (lastVersionTag.value !== versionTag && versionTag) {
|
||||
clearInterval(timer.value);
|
||||
stop();
|
||||
handleNotice(versionTag);
|
||||
}
|
||||
}
|
||||
|
||||
async function runUpdateCheck() {
|
||||
if (isCheckingUpdates) return;
|
||||
isCheckingUpdates = true;
|
||||
try {
|
||||
await checkForUpdates();
|
||||
} finally {
|
||||
isCheckingUpdates = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleNotice(versionTag: string) {
|
||||
currentVersionTag.value = versionTag;
|
||||
modalApi.open();
|
||||
}
|
||||
|
||||
function start() {
|
||||
stop();
|
||||
if (props.checkUpdatesInterval <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 每 checkUpdatesInterval(默认值为1) 分钟检查一次
|
||||
timer.value = setInterval(
|
||||
checkForUpdates,
|
||||
() => void runUpdateCheck(),
|
||||
props.checkUpdatesInterval * 60 * 1000,
|
||||
);
|
||||
}
|
||||
@@ -96,18 +124,21 @@ function handleVisibilitychange() {
|
||||
if (document.hidden) {
|
||||
stop();
|
||||
} else {
|
||||
if (!isCheckingUpdates) {
|
||||
isCheckingUpdates = true;
|
||||
checkForUpdates().finally(() => {
|
||||
isCheckingUpdates = false;
|
||||
runUpdateCheck().finally(() => {
|
||||
if (!document.hidden) {
|
||||
start();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (timer.value !== undefined) {
|
||||
clearInterval(timer.value);
|
||||
timer.value = undefined;
|
||||
}
|
||||
versionCheckController?.abort();
|
||||
versionCheckController = null;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
Reference in New Issue
Block a user