feat: 接入分镜单集重生成接口

接入后端正式单集 Storyboard Shot 重生成能力,补齐校验回执、旧 Shot 缓存失效与页面刷新逻辑。
This commit is contained in:
GouJ
2026-09-08 20:09:37 +08:00
committed by GitHub
parent b4e1e6873c
commit 6dc3a93348
8 changed files with 159 additions and 10 deletions
+42 -1
View File
@@ -15,6 +15,7 @@ import ShotTools from '@/features/storyboard/components/ShotTools.vue'
import {
designedShot,
directionsResult,
episodeShots,
storyboardCheckpoint,
visualStatesResult
} from '@/features/storyboard/testing/fixtures'
@@ -141,7 +142,7 @@ afterEach(() => {
wrapper = undefined
document.body.innerHTML = ''
vi.unstubAllGlobals()
Object.assign(getStoryboardSession(fixture.id), { receipt: null, prompts: {} })
Object.assign(getStoryboardSession(fixture.id), { receipt: null, prompts: {}, regeneratedEpisodes: {} })
getImageSession(fixture.id).receipt = null
getImageSession(fixture.id).promptReceipt = null
Object.assign(getOperation(fixture.id), { pending: false, label: '', error: '', notice: '' })
@@ -1190,6 +1191,46 @@ describe('工作台页面交互', () => {
expect(drawerPanel().text()).toContain('缺少主体状态')
})
it('重生成当前集镜头后刷新正式设计,并用接口回执替换过期 Breakdown 文本', async () => {
const regenerated = episodeShots()
regenerated.beatShots[0]!.shots[0]!.title = '重生成后的开场镜头'
regenerated.beatShots[0]!.shots[0]!.description = '新的镜头正文'
const fetcher = vi.fn<typeof fetch>().mockImplementation(async (url, init) => {
if (init?.method === 'POST')
return jsonResponse({
projectId: fixture.id,
episodeNo: 1,
episodeShot: regenerated,
storyboardShotSummary: {
episodeCount: 1,
beatCount: 2,
shotCount: 2,
totalDurationSeconds: 10,
subjectRefCount: 2,
subjectBindingCount: 2,
bindingCoverage: 1
},
storyboardShotValidation: { valid: true, issues: [] }
})
return jsonResponse(
String(url).includes('storyboard-directions')
? directionsResult(fixture.id, 1, false)
: visualStatesResult(fixture.id)
)
})
vi.stubGlobal('fetch', fetcher)
await mountStoryboard()
await confirmGeneration('重生成第 1 集镜头')
const post = fetcher.mock.calls.find(([, init]) => init?.method === 'POST')!
expect(post[0]).toBe('/api/projects/page-test-project/storyboard-shots/regenerate')
expect(JSON.parse(post[1]!.body as string)).toEqual({ episodeNo: 1 })
expect(wrapper!.text()).toContain('重生成后的开场镜头')
expect(wrapper!.text()).toContain('新的镜头正文')
expect(drawerPanel().text()).toContain('本集新镜头已保存')
expect(drawerPanel().text()).toContain('2 个镜头')
expect(fetcher.mock.calls.filter(([, init]) => !init?.method || init.method === 'GET')).toHaveLength(4)
})
it('批量 200 部分失败不显示全成功,默认补齐与显式覆盖参数独立', async () => {
const fetcher = vi.fn<typeof fetch>().mockImplementation(async (url, init) => {
if (init?.method === 'POST')
+2
View File
@@ -46,9 +46,11 @@ describe('后端 API 契约', () => {
it('分镜只调用正式项目接口,并保持 force 和零次修复参数', async () => {
const fetcher = vi.fn<typeof fetch>().mockImplementation(async () => new Response('{"data":{}}'))
vi.stubGlobal('fetch', fetcher)
await storyboardApi.regenerateEpisodeShots('a/b', 3)
await storyboardApi.generateDirections('a/b', { concurrency: 2, force: false })
await storyboardApi.generateVisualStates('p', { concurrency: 3, force: true, maxRepairAttempts: 0 })
expect(fetcher.mock.calls.map(([url, init]) => [url, init?.method, JSON.parse(init!.body as string)])).toEqual([
['/api/projects/a%2Fb/storyboard-shots/regenerate', 'POST', { episodeNo: 3 }],
['/api/projects/a%2Fb/storyboard-directions/generate', 'POST', { concurrency: 2, force: false }],
[
'/api/projects/p/storyboard-visual-states/generate',