feat: 同步最新生图与身份能力

This commit is contained in:
GouJ
2026-09-22 19:56:12 +08:00
parent 65e8f9c3ba
commit be06caaa9f
18 changed files with 583 additions and 69 deletions
@@ -140,4 +140,59 @@ describe('镜头生产数据契约', () => {
await flushPromises()
expect(wrapper.emitted('generate')?.at(-1)).toEqual([{ setPrimary: false }])
})
it('首帧生成前可预览后端最终选图规划和 Prompt', async () => {
const fetcher = vi.fn<typeof fetch>().mockResolvedValue(
new Response(
JSON.stringify({
data: {
shotId: 'shot-1',
episodeNo: 1,
beatNo: 1,
shotNo: 1,
provider: 'seedream',
model: 'image-model',
generationConfig: {},
references: [],
referencePlan: {
provider: 'seedream',
maxReferenceImages: 4,
totalCandidates: 2,
strategy: 'subject-coverage',
selected: [
{
subjectRef: '@CH0001',
module: 'character',
role: 'identity-anchor',
imageId: 'anchor-1'
}
],
omitted: [
{
subjectRef: '@SC0001',
module: 'scene',
role: 'form-primary',
imageId: 'scene-1'
}
]
},
providerPrompt: '最终首帧 Prompt'
}
})
)
)
vi.stubGlobal('fetch', fetcher)
wrapper = mount(KeyframeDialog, {
attachTo: document.body,
props: { open: true, shotId: 'shot-1', shotTitle: '开场', keyframes: [], disabled: false }
})
await flushPromises()
button('预览实际生成输入').click()
await flushPromises()
expect(fetcher.mock.calls[0]?.[0]).toBe('/api/storyboard-shots/shot-1/keyframe/preview')
expect(document.body.textContent).toContain('已选 1 / 2 张')
expect(document.body.textContent).toContain('省略 1 张')
expect(document.body.textContent).toContain('最终首帧 Prompt')
expect(wrapper.emitted('generate')).toBeUndefined()
})
})
@@ -70,6 +70,41 @@ describe('形态图片选择与操作', () => {
expect(document.body.textContent).toContain('母版')
expect(document.body.textContent).not.toContain('道具形态生图暂不自动引用')
})
it('按需预览正式形态的实际参考图和 Provider Prompt,不触发生图事件', async () => {
const fetcher = vi.fn<typeof fetch>().mockResolvedValue(
new Response(
JSON.stringify({
data: {
subjectFormId: 'form-db-1',
provider: 'seedream',
model: 'image-model',
generationConfig: { width: 2048 },
references: [
{
imageNumber: 1,
role: 'identity-anchor',
imageId: 'anchor-1',
imageUrl: '/storage/anchor.png'
}
],
providerPrompt: '最终形态 Prompt'
}
})
)
)
vi.stubGlobal('fetch', fetcher)
wrapper = mount(GenerateImageDialog, {
attachTo: document.body,
props: { open: true, form: formFixture(), disabled: false }
})
await flushPromises()
button('预览实际生成输入').click()
await flushPromises()
expect(fetcher.mock.calls[0]?.[0]).toBe('/api/subject-forms/form-db-1/images/preview')
expect(document.body.textContent).toContain('seedream / image-model')
expect(document.body.textContent).toContain('最终形态 Prompt')
expect(wrapper.emitted('generate')).toBeUndefined()
})
it('已有主图优先于新候选图和失败记录,无主图才回退到成功候选图', () => {
const form = formFixture()
form.images.unshift(
@@ -112,6 +112,14 @@ describe('形态正式提示词与批量配置', () => {
expect(service.session.value.promptReceipt?.result.failures[0]?.error).toBe('模型拒绝')
expect(service.session.value.receipt).toBeNull()
})
it('批量提示词和图片可按主体模块缩小范围', async () => {
const { service, posts } = await setup()
service.batchModule.value = 'scene'
await service.generatePrompts()
await service.generateProject()
expect(JSON.parse(String(posts()[0]?.[1]?.body))).toMatchObject({ module: 'scene' })
expect(JSON.parse(String(posts()[1]?.[1]?.body))).toMatchObject({ module: 'scene' })
})
it('图片数量上限可选,非法上限和并发阻止提交', async () => {
const { service, posts } = await setup()
for (const limit of [0, -1, 1.5]) {
+26 -2
View File
@@ -4,6 +4,8 @@ import { projectsApi } from '@/features/projects/api'
import { breakdownApi } from '@/features/breakdown/api'
import { storyboardApi } from '@/features/storyboard/api'
import { subjectImagesApi } from '@/features/subject-images/api'
import { subjectIdentityApi } from '@/features/subject-identity/api'
import { productionApi } from '@/features/production/api'
afterEach(() => {
vi.unstubAllGlobals()
@@ -43,6 +45,29 @@ describe('后端 API 契约', () => {
})
expect(fetcher.mock.calls[4]![1]!.body).toBeUndefined()
})
it('新增预览、模块批处理和稳定母版接口使用正式数据库 ID', async () => {
const fetcher = vi.fn<typeof fetch>().mockImplementation(async () => new Response('{"data":{}}'))
vi.stubGlobal('fetch', fetcher)
await subjectImagesApi.preview('form/id')
await subjectImagesApi.generatePrompts('project/id', { concurrency: 2, force: false, module: 'scene' })
await subjectImagesApi.generateProject('project/id', { concurrency: 1, force: false, module: 'prop' })
await subjectIdentityApi.generateProject('project/id', { concurrency: 2, force: false, module: 'scene' })
await subjectIdentityApi.generateStableAnchors('project/id', { module: 'prop', limit: 3, concurrency: 2 })
await productionApi.previewKeyframe('shot/id')
expect(fetcher.mock.calls.map(([url, init]) => [url, init?.method])).toEqual([
['/api/subject-forms/form%2Fid/images/preview', 'POST'],
['/api/projects/project%2Fid/subject-forms/generation-prompts', 'POST'],
['/api/projects/project%2Fid/subject-images/generate', 'POST'],
['/api/projects/project%2Fid/subject-identities/generate', 'POST'],
['/api/projects/project%2Fid/subject-identities/anchors/generate', 'POST'],
['/api/storyboard-shots/shot%2Fid/keyframe/preview', 'POST']
])
expect(JSON.parse(fetcher.mock.calls[4]![1]!.body as string)).toEqual({
module: 'prop',
limit: 3,
concurrency: 2
})
})
it('分镜只调用正式项目接口,并保持 force 和零次修复参数', async () => {
const fetcher = vi.fn<typeof fetch>().mockImplementation(async () => new Response('{"data":{}}'))
vi.stubGlobal('fetch', fetcher)
@@ -110,12 +135,11 @@ describe('后端 API 契约', () => {
)
vi.stubGlobal('fetch', fetcher)
expect(await projectsApi.list()).toEqual([{ id: 'p1' }])
expect(await projectsApi.create({ title: '测试剧本', topic: '故事', style: '悬疑', episodeCount: 3 })).toEqual({
expect(await projectsApi.create({ topic: '故事', style: '悬疑', episodeCount: 3 })).toEqual({
projectId: 'p2',
status: 'generating'
})
expect(JSON.parse(fetcher.mock.calls[1]![1].body as string)).toEqual({
title: '测试剧本',
topic: '故事',
style: '悬疑',
episodeCount: 3