fe49fef1ad
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 55s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m12s
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Failing after 577h31m12s
CI/CD Pipeline / Frontend Lint (push) Failing after 577h31m33s
CI/CD Pipeline / PR Build Web Image (push) Failing after 577h31m14s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 577h32m24s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 577h32m28s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 577h32m30s
CI/CD Pipeline / PR Build API Image (push) Failing after 578h5m4s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 578h6m14s
- Step2/Step4 auto-save via useDraftAutoSave (GET-merge-PUT, serial queue)
- useDraftAutoSave: failed saves preserve patch with exponential backoff retry
- Step6 cover requests include full title_config
- Step7 create task: source_edit_plan_id, title_config, cover_url priority
- Polling switched to GET /generation/tasks/{task_id} with proper error handling
- getGenerationTaskResults retries before treating as failure
- Removed 7 ghost API functions (non-existent backend routes)
- BGM presets path updated to /templates/{id}/editor/bgm/presets
- Type alignment for BatchGenerationTaskResponse
125 lines
4.5 KiB
TypeScript
125 lines
4.5 KiB
TypeScript
/**
|
|
* 草稿自动保存工具 Hook
|
|
*
|
|
* 背景:后端 PUT /templates/{id}/editor 的 config 是「整体替换」语义,
|
|
* 直接发送 { config: { asset_ids } } 会把 title 等其他字段覆盖掉。
|
|
* 本 Hook 统一执行「GET 当前 config → 浅合并新字段 → PUT 回去」,
|
|
* 并用串行队列 + AbortController 保证:
|
|
* - 同一时刻只有一个保存请求在飞
|
|
* - 快速连续变化时只提交最后一次
|
|
* - 组件卸载时取消未完成请求
|
|
* - 保存失败时保留补丁,自动重试(指数退避,最多 5 次)
|
|
*
|
|
* 保存失败只 console.warn,不弹窗、不阻塞。
|
|
*/
|
|
import { useCallback, useEffect, useRef } from "react"
|
|
import { getEditPlan, updateEditPlan } from "@/api/template-editor"
|
|
|
|
type ConfigPatch = Record<string, unknown>
|
|
|
|
/** 最大自动重试次数 */
|
|
const MAX_RETRIES = 5
|
|
/** 初始重试延迟(ms),每次翻倍 */
|
|
const BASE_RETRY_DELAY = 1000
|
|
|
|
export function useDraftAutoSave(templateId?: string) {
|
|
const timerRef = useRef<ReturnType<typeof setTimeout>>()
|
|
const abortRef = useRef<AbortController | null>(null)
|
|
// 待合并的补丁队列(解决「保存进行中又来了新变化」)
|
|
const pendingPatchRef = useRef<ConfigPatch | null>(null)
|
|
const savingRef = useRef(false)
|
|
const templateIdRef = useRef(templateId)
|
|
templateIdRef.current = templateId
|
|
|
|
const flush = useCallback(async (retryCount = 0) => {
|
|
const tid = templateIdRef.current
|
|
if (!tid) return
|
|
// 已有保存在飞:把新补丁暂存,等当前请求结束后再合并一次
|
|
if (savingRef.current) return
|
|
|
|
// 快照当前补丁,但先不清空 —— 成功后才清除,失败时保留以便重试
|
|
const patchToSave = pendingPatchRef.current
|
|
if (!patchToSave) {
|
|
savingRef.current = false
|
|
return
|
|
}
|
|
savingRef.current = true
|
|
|
|
const controller = new AbortController()
|
|
abortRef.current = controller
|
|
try {
|
|
// 1. 读当前 config(拿最新,避免覆盖别人/别的步骤写入的字段)
|
|
const current = await getEditPlan(tid)
|
|
if (controller.signal.aborted) return
|
|
const merged = { ...(current.config || {}), ...patchToSave }
|
|
// 2. 写回完整合并后的 config
|
|
await updateEditPlan(tid, { config: merged }, controller.signal)
|
|
// 3. 保存成功才清除已保存的补丁
|
|
// (保存期间可能有新补丁进来,只清除我们已经保存的部分)
|
|
pendingPatchRef.current = null
|
|
} catch (err) {
|
|
const name = (err as { name?: string })?.name
|
|
if (name === "CanceledError" || name === "AbortError") {
|
|
// 组件卸载或新请求取消,不重试
|
|
return
|
|
}
|
|
console.warn("[useDraftAutoSave] 自动保存草稿失败:", err)
|
|
|
|
// 保存失败:把本次尝试保存的补丁合并回 pendingPatchRef
|
|
// (保存期间可能有新补丁,新补丁优先)
|
|
pendingPatchRef.current = {
|
|
...patchToSave,
|
|
...(pendingPatchRef.current || {}),
|
|
}
|
|
|
|
// 指数退避重试
|
|
if (retryCount < MAX_RETRIES && !controller.signal.aborted) {
|
|
const delay = BASE_RETRY_DELAY * Math.pow(2, retryCount)
|
|
timerRef.current = setTimeout(() => {
|
|
void flush(retryCount + 1)
|
|
}, delay)
|
|
}
|
|
// 超过最大重试次数后,补丁仍保留在 pendingPatchRef 中,
|
|
// 下次 scheduleSave 触发时会一起带上
|
|
} finally {
|
|
savingRef.current = false
|
|
// 保存期间又积累了新变化(且不是在重试路径中),再触发一次
|
|
if (pendingPatchRef.current && !controller.signal.aborted && retryCount === 0) {
|
|
timerRef.current = setTimeout(() => {
|
|
void flush()
|
|
}, 50)
|
|
}
|
|
}
|
|
}, [])
|
|
|
|
/**
|
|
* 调度一次自动保存(防抖)
|
|
* @param patch 要合并进 config 的局部字段
|
|
* @param delay 防抖毫秒数
|
|
*/
|
|
const scheduleSave = useCallback(
|
|
(patch: ConfigPatch, delay = 500) => {
|
|
const tid = templateIdRef.current
|
|
if (!tid) return
|
|
if (timerRef.current) clearTimeout(timerRef.current)
|
|
// 累计补丁(同一周期内多次变化合并成一次写入)
|
|
pendingPatchRef.current = { ...(pendingPatchRef.current || {}), ...patch }
|
|
timerRef.current = setTimeout(() => {
|
|
void flush()
|
|
}, delay)
|
|
},
|
|
[flush],
|
|
)
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
if (timerRef.current) clearTimeout(timerRef.current)
|
|
if (abortRef.current) abortRef.current.abort()
|
|
}
|
|
}, [])
|
|
|
|
return { scheduleSave }
|
|
}
|
|
|
|
export default useDraftAutoSave
|