diff --git a/src/features/subject-images/useSubjectImages.ts b/src/features/subject-images/useSubjectImages.ts index 9bba2ad..e4e5877 100644 --- a/src/features/subject-images/useSubjectImages.ts +++ b/src/features/subject-images/useSubjectImages.ts @@ -5,8 +5,8 @@ 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' +import { getImageSession, hasRunningImages, isPrimaryIdentityStale } from './model' +import type { GenerateFormImageInput, SubjectFormAsset } from './types' /** 形态图库统一读取数据库记录并管理单图/批量长请求,不调用生产流程。 */ export function useSubjectImages() { @@ -37,6 +37,7 @@ export function useSubjectImages() { } }) 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))) @@ -87,9 +88,59 @@ export function useSubjectImages() { 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: false + }) + 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, @@ -99,6 +150,31 @@ export function useSubjectImages() { running, batchValid, generate, - generateProject + 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 +}