Files
short-drama-agent-front/src/features/subject-images/useSubjectImages.ts
T
GouJ 47443c0efe feat: 接入形态图库与 Seedream 生图操作
展示已有主图、候选图和失败记录,支持单图及批量生成、主图选择。
补充正式 ID 校验、费用确认、图片加载占位和回归测试。
依赖后端 eac8dc3 的项目形态图库只读接口。
2026-08-28 15:16:25 +08:00

105 lines
4.4 KiB
TypeScript

import { computed, ref } from 'vue'
import { usePolling } from '../../composables/usePolling'
import { ApiError } from '../../lib/http'
import { useProjectContext } from '../projects/context'
import { getOperation, runOperation } from '../workflows/operations'
import { workflowCheckpoints } from '../workflows/selectors'
import { subjectImagesApi } from './api'
import { getImageSession, hasRunningImages } from './model'
import type { GenerateFormImageInput } from './types'
/** 形态图库统一读取数据库记录并管理单图/批量长请求,不调用生产流程。 */
export function useSubjectImages() {
const context = useProjectContext()
const id = computed(() => context.project.value?.id ?? '')
const concurrency = ref(2)
const force = ref(false)
const query = usePolling(id, async (projectId, signal) => {
if (!projectId) return []
try {
const forms = await subjectImagesApi.listForms(projectId, signal)
if (
forms.some(
form =>
form.subject.projectId !== projectId ||
form.images.some(image => image.subjectFormId !== form.id)
)
)
throw new Error('形态图库返回了不匹配的项目或图片,请刷新后重试。')
return forms
} catch (error) {
if (error instanceof ApiError && error.status === 404)
throw new Error(
'后端尚未提供形态图库查询,请更新后端 dev 并重启服务(GET /projects/:id/subject-forms)。',
{ cause: error }
)
throw error
}
})
const forms = computed(() => query.data.value ?? [])
const operation = computed(() => getOperation(id.value))
const session = computed(() => getImageSession(id.value))
const running = computed(() => forms.value.some(form => hasRunningImages(form.images)))
const breakdownRunning = computed(
() =>
workflowCheckpoints(context.checkpoints.value, 'breakdown').at(-1)?.state.workflowExecution?.status ===
'running'
)
const blocked = computed(
() =>
operation.value.pending ||
!!context.error.value ||
!!query.error.value ||
query.data.value === null ||
running.value ||
breakdownRunning.value ||
context.project.value?.status === 'generating'
)
const batchValid = computed(() => Number.isSafeInteger(concurrency.value) && concurrency.value > 0)
/** 捕获正式 Form ID,生成后重新读取图片;刷新失败也不自动重发有费用的请求。 */
async function generate(formId: string, input: GenerateFormImageInput) {
if (blocked.value) return
const form = forms.value.find(item => item.id === formId)
if (!form) return
const projectId = id.value
await runOperation(projectId, `生成 ${form.subject.name} · ${form.name} 图片`, async () => {
const image = await subjectImagesApi.generate(formId, input)
if (!image || image.subjectFormId !== formId || image.status !== 'completed' || !image.imageUrl)
throw new Error(image?.error || '接口未返回已完成的图片,请先刷新图片记录核对后端状态。')
})
await query.refresh()
}
/** 批量始终面向整个项目,不受页面筛选影响;force 仅新增候选图,不替换已有主图。 */
async function generateProject() {
if (blocked.value || !batchValid.value || !forms.value.length) return
const projectId = id.value
const input = { provider: 'seedream' as const, concurrency: concurrency.value, force: force.value }
const target = getImageSession(projectId)
target.receipt = null
await runOperation(projectId, '批量生成项目形态图片', async () => {
target.receipt = {
title: input.force ? '为全项目新增候选图' : '补齐项目主参考图',
result: await subjectImagesApi.generateProject(projectId, input)
}
})
await query.refresh()
}
return {
id,
forms,
query,
operation,
session,
concurrency,
force,
blocked,
running,
batchValid,
generate,
generateProject
}
}