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, isPrimaryIdentityStale } from './model' import type { GenerateFormImageInput, SubjectFormAsset } 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 staleForms = computed(() => forms.value.filter(form => isPrimaryIdentityStale(form))) 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() } /** * 只刷新 Identity Anchor 已变化的 Character Form。 * 用户已经明确确认“刷新过期形态”,因此成功的新图直接切换为当前 Primary;旧图仍保留为历史记录。 */ async function generateStale() { if (blocked.value || !batchValid.value || !staleForms.value.length) return const projectId = id.value const targets = [...staleForms.value] const receipt = getImageSession(projectId) receipt.receipt = null await runOperation(projectId, '刷新过期人物形态图', async () => { const results = await runWithConcurrency(targets, concurrency.value, async form => { try { const image = await subjectImagesApi.generate(form.id, { provider: 'seedream', setPrimary: true }) if (!image || image.subjectFormId !== form.id || image.status !== 'completed' || !image.imageUrl) { throw new Error(image?.error || '接口未返回已完成的主参考图') } return { subjectFormId: form.id, success: true as const } } catch (error) { return { subjectFormId: form.id, success: false as const, error: error instanceof Error ? error.message : String(error) } } }) const failures = results .filter(item => !item.success) .map(item => ({ subjectFormId: item.subjectFormId, error: item.error })) receipt.receipt = { title: '刷新过期人物形态图', result: { total: forms.value.length, targetCount: targets.length, generated: results.filter(item => item.success).length, skipped: forms.value.length - targets.length, failed: failures.length, failures } } }) await query.refresh() } return { id, forms, staleForms, query, operation, session, concurrency, force, blocked, running, batchValid, generate, generateProject, generateStale } } /** 使用固定并发数执行需要付费的单形态生图请求。 */ async function runWithConcurrency( items: SubjectFormAsset[], concurrency: number, handler: (item: SubjectFormAsset) => Promise ): Promise { const results: TResult[] = [] let currentIndex = 0 async function worker() { while (currentIndex < items.length) { const index = currentIndex currentIndex += 1 const item = items[index] if (!item) continue results[index] = await handler(item) } } const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker()) await Promise.all(workers) return results }