feat: add generation job cancel and retry queue

This commit is contained in:
2026-04-19 18:45:34 +08:00
parent 6fb128955f
commit b89ca96e4b
18 changed files with 756 additions and 51 deletions

View File

@@ -40,10 +40,13 @@ const userStore = useUserStore()
// State
const inputType = ref<'keywords' | 'full_story'>('keywords')
const outputMode = ref<'full_story' | 'storybook'>('full_story')
const inputData = ref('')
const educationTheme = ref('')
const loading = ref(false)
const error = ref('')
const inputData = ref('')
const educationTheme = ref('')
const loading = ref(false)
const canceling = ref(false)
const cancelRequested = ref(false)
const error = ref('')
const activeGenerationJobId = ref<string | null>(null)
// Data
interface ChildProfile {
@@ -110,10 +113,17 @@ interface GenerationAcceptedResponse {
interface GenerationJobDetail {
story_id: number | null
status: string
current_step: string
is_terminal: boolean
error_message: string | null
}
interface GenerationJobActionResponse {
status: string
current_step: string
}
const JOB_POLL_INTERVAL_MS = 1500
const JOB_POLL_MAX_ATTEMPTS = 80
@@ -121,6 +131,9 @@ const JOB_POLL_MAX_ATTEMPTS = 80
function close() {
emit('update:modelValue', false)
error.value = ''
activeGenerationJobId.value = null
cancelRequested.value = false
canceling.value = false
}
function sleep(ms: number) {
@@ -132,6 +145,9 @@ function sleep(ms: number) {
async function waitForStoryId(jobId: string) {
for (let attempt = 0; attempt < JOB_POLL_MAX_ATTEMPTS; attempt += 1) {
const detail = await api.get<GenerationJobDetail>(`/api/generations/jobs/${jobId}`)
if (detail.status === 'canceled' || detail.current_step === 'generation_canceled') {
return null
}
if (detail.story_id) {
return detail.story_id
}
@@ -143,6 +159,27 @@ async function waitForStoryId(jobId: string) {
throw new Error('任务已提交,但主内容落库超时,请稍后到故事库查看最新结果')
}
async function cancelGenerationJob() {
if (!activeGenerationJobId.value || canceling.value || cancelRequested.value) return
canceling.value = true
error.value = ''
try {
const result = await api.post<GenerationJobActionResponse>(
`/api/generations/jobs/${activeGenerationJobId.value}/cancel`,
)
cancelRequested.value = true
if (result.status === 'canceled' || result.current_step === 'generation_canceled') {
loading.value = false
close()
}
} catch (e) {
error.value = e instanceof Error ? e.message : '取消任务失败'
} finally {
canceling.value = false
}
}
async function fetchProfiles() {
if (!userStore.user) return
@@ -191,7 +228,9 @@ async function generateStory() {
return
}
loading.value = true
loading.value = true
cancelRequested.value = false
activeGenerationJobId.value = null
error.value = ''
try {
@@ -211,8 +250,13 @@ async function generateStory() {
if (!jobId) {
throw new Error('生成任务已创建,但缺少任务编号')
}
activeGenerationJobId.value = jobId
const storyId = accepted.id ?? await waitForStoryId(jobId)
if (storyId === null) {
close()
return
}
close()
if (requestedOutputMode.value === 'storybook') {
router.push(`/storybook/view/${storyId}`)
@@ -222,9 +266,11 @@ async function generateStory() {
} catch (e) {
error.value = e instanceof Error ? e.message : '生成失败'
} finally {
loading.value = false
}
}
loading.value = false
activeGenerationJobId.value = null
cancelRequested.value = false
}
}
</script>
<template>
@@ -253,6 +299,22 @@ async function generateStory() {
:title="generationTitle"
:steps="generationSteps"
/>
<div
v-if="loading && activeGenerationJobId"
class="fixed bottom-10 z-[110] flex flex-col items-center gap-3"
>
<BaseButton
variant="secondary"
:loading="canceling"
:disabled="cancelRequested"
@click="cancelGenerationJob"
>
{{ cancelRequested ? '正在取消任务...' : '取消任务' }}
</BaseButton>
<p class="text-sm text-white/70">
{{ cancelRequested ? '已提交取消请求,会在安全检查点停止任务。' : '如果是误触发起,可以现在取消后台任务。' }}
</p>
</div>
<!-- 模态框内容 -->
<div v-else class="relative w-full max-w-2xl max-h-[90vh] overflow-y-auto bg-[#1C2035] border border-gray-700/50 rounded-3xl shadow-2xl p-6 md:p-8">