feat: 形态图库支持批量刷新过期主图
This commit is contained in:
@@ -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<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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user