feat: 形态图库支持批量刷新过期主图

This commit is contained in:
GouJ
2026-09-01 10:52:13 +08:00
parent 106e3b91a6
commit 6fa384eab0
@@ -5,8 +5,8 @@ import { useProjectContext } from '../projects/context'
import { getOperation, runOperation } from '../workflows/operations' import { getOperation, runOperation } from '../workflows/operations'
import { workflowCheckpoints } from '../workflows/selectors' import { workflowCheckpoints } from '../workflows/selectors'
import { subjectImagesApi } from './api' import { subjectImagesApi } from './api'
import { getImageSession, hasRunningImages } from './model' import { getImageSession, hasRunningImages, isPrimaryIdentityStale } from './model'
import type { GenerateFormImageInput } from './types' import type { GenerateFormImageInput, SubjectFormAsset } from './types'
/** 形态图库统一读取数据库记录并管理单图/批量长请求,不调用生产流程。 */ /** 形态图库统一读取数据库记录并管理单图/批量长请求,不调用生产流程。 */
export function useSubjectImages() { export function useSubjectImages() {
@@ -37,6 +37,7 @@ export function useSubjectImages() {
} }
}) })
const forms = computed(() => query.data.value ?? []) const forms = computed(() => query.data.value ?? [])
const staleForms = computed(() => forms.value.filter(form => isPrimaryIdentityStale(form)))
const operation = computed(() => getOperation(id.value)) const operation = computed(() => getOperation(id.value))
const session = computed(() => getImageSession(id.value)) const session = computed(() => getImageSession(id.value))
const running = computed(() => forms.value.some(form => hasRunningImages(form.images))) const running = computed(() => forms.value.some(form => hasRunningImages(form.images)))
@@ -87,9 +88,59 @@ export function useSubjectImages() {
await query.refresh() 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 { return {
id, id,
forms, forms,
staleForms,
query, query,
operation, operation,
session, session,
@@ -99,6 +150,31 @@ export function useSubjectImages() {
running, running,
batchValid, batchValid,
generate, generate,
generateProject generateProject,
generateStale
} }
} }
/** 使用固定并发数执行需要付费的单形态生图请求。 */
async function runWithConcurrency<TResult>(
items: SubjectFormAsset[],
concurrency: number,
handler: (item: SubjectFormAsset) => Promise<TResult>
): Promise<TResult[]> {
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
}