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