移除视频素材的快速编辑支持
Project CI / Repository checks (pull_request) Failing after 43s
Project CI / Backend tests (pull_request) Successful in 3m48s
Project CI / Frontend tests (pull_request) Successful in 4m0s
Project CI / Native shell tests (pull_request) Successful in 16m16s

清理相关逻辑
清理测试用例。
This commit is contained in:
2026-08-10 12:00:10 +08:00
parent 8abeb333bb
commit 8f2b889160
15 changed files with 374 additions and 392 deletions
@@ -13755,14 +13755,15 @@
## 2026-08-04 图片画布快速编辑改用统一正向白名单(2026-08-05 修订)
- 背景:画布生成结果统一显示快速编辑,但部分角色动作 / 序列帧和音频结果会进入不受支持的图片编辑链路;不同入口各自判断时也容易继续漂移。
- 决策:快速编辑只支持普通静态图片、角色图、规范图、完整图标图集、图标规范、UI 设计图、宣发图游戏场景图视频单个拆分图标、角色动作 / 序列帧、音效与背景音乐不支持;新增媒体或素材类型默认不开放。浮动工具栏、图层右键菜单、独立图片菜单、打开面板入口和提交门禁统一调用同一个正向白名单;后端图片编辑 BFF 基于目标图层的有效素材类型与媒体类型执行同一正向门禁,视频快速编辑只走视频生成接口
- 决策:快速编辑只支持普通静态图片、角色图、规范图、完整图标图集、图标规范、UI 设计图、宣发图游戏场景图视频单个拆分图标、角色动作 / 序列帧、音效与背景音乐不支持;新增媒体或素材类型默认不开放。浮动工具栏、图层右键菜单、独立图片菜单、打开面板入口和提交门禁统一调用同一个正向白名单;后端图片编辑 BFF 基于目标图层的有效素材类型与媒体类型执行同一正向门禁。
- 边界:角色动作继续通过对应的动作生成链路处理,不再把当前帧当作可快速编辑图片。
- 2026-08-06 修订:画布 Agent 的 `edit_image` 只接受图片输入,新任务以 `assetKind=null` 表示普通静态图片,不再使用 synthetic `editor_agent_edit_image`。worker 仅按服务端生成的 `editor-agent:` dedupe namespace 识别并归一历史排队 payload;普通调用伪造旧值继续被拒绝。已持久化资源中的旧值只有在后端从真实目标图层 / 项目资源解析后才兼容为空类型,避免历史 Agent 结果失去快速编辑能力,同时不扩大请求白名单。
- 2026-08-07 修订:站内与 External v1 图片编辑请求统一只接受必填 `sourceReferenceId`,且该值必须是当前账号已登记的项目资源 ID 或素材 IDobjectKey、URL、Data URL、Blob URL 以及旧 `sourceImageSrc/sourceResourceId/assetKind` 字段全部返回 400,不提供兼容别名。后端用共享窄查询分别按两张表主键定点解析,双表同 ID、未命中、跨账号、对象缺失或越权均失败关闭;权威类型完全来自业务记录,只允许普通静态图片、规范图、角色图、完整图标图集、图标规范、宣发图和 UI 设计图。请求带 `targetLayerId` 时必须同时带 `projectId`,来源与目标优先比较 `assetObjectId`,任一方缺失才比较 canonical `(bucket, objectKey)`,且来源默认类型必须与目标资源默认类型一致;最终类型取目标覆盖值或目标资源类型。HTTP 入队写入版本化服务端解析快照,worker 执行前按同一业务 ID 再次定点解析,身份或类型漂移即失败关闭。旧任务只把已有资源 ID 或旧来源字符串本身当业务 ID 迁移,绝不按 objectKey 反查。Canvas Agent 必须从 `ImageMetadata.reference_id` 取主来源;红框标注上传图只作为辅助 `referenceImageSrcs`,不能冒充被编辑资源。仅以素材 ID 编辑时,队列审计与 `generationInputs.references` 保留素材 ID,不伪造项目资源关系。
- 2026-08-08 修订:`scene` 是单张静态图片素材,加入前端快速编辑正向白名单和 api-server 权威来源白名单;编辑结果继续保留 `scene`。用户标签覆盖侧的前端菜单与 SpacetimeDB 结构化布局白名单也必须显式覆盖 `scene`。通用图片生成接口仍拒绝 `scene`,避免绕过结构化场景生成契约。
- 2026-08-08 修订:画布 Agent 的 `edit_image` 工具内部可先形成待确认的 `EditorImageEditRequest`,但确认接口必须在通用入队前复用站内图片编辑的来源解析与目标预检,写入 `{ version, request, source }` 服务端快照。worker 只解析正式 versioned payload 与既有历史 payload,不接受当前 direct request 作为 fallback;这样未上线的 Agent 路径在生产端原位修正,不扩大消费端协议。
- 2026-08-08 修订:图片编辑业务引用按 `objectKey` 找到权威 `asset_object` 后,若资源或素材记录同时保存了 `assetObjectId`,必须验证两者指向同一对象;不一致时在 SpacetimeDB resolver 边界失败关闭,不能把未验证的记录 ID 与已验证的对象路径组合进 snapshot。缺少 ID 的历史记录继续以 canonical `(bucket, objectKey)` 作为对象身份。
- 验证:模型测试覆盖允许与拒绝类型,工具栏和两类右键菜单测试覆盖单个拆分图标、角色动作及音频不展示,提交工作流测试覆盖单个拆分图标和角色动作绕过入口时仍拒绝;后端表驱动测试覆盖全部现役素材 / 媒体类型与未知类型,锁定图片编辑端点失败关闭
- 2026-08-10 永久修订:画布视频快速编辑永久下线,上传、生成和历史视频图层均不再展示入口,程序化打开与直接提交同样失败关闭;不设置 feature flag、兼容桥或数据迁移。普通视频生成、视频“改造”、下载、`referenceVideoSrcs` 以及 V2 `videoReference` 恢复保持不变。External v1 从未正式声明“视频快速编辑”,只提供带可选 `referenceVideoSrcs` 的通用视频生成,因此不修改 `/api/external/v1` 路由、DTO、异步语义或 OpenAPI。当前服务已完全停止且无在途 / 可重试任务,不新增旧任务收口逻辑
- 验证:模型测试覆盖允许与拒绝类型,工具栏和两类右键菜单覆盖视频、单个拆分图标、角色动作及音频不展示,打开与提交工作流覆盖视频等不支持类型绕过入口时仍拒绝;后端表驱动测试覆盖全部现役图片素材 / 媒体类型与未知类型,锁定图片编辑端点失败关闭。
- 关联:`src/components/image-editor/ImageCanvasGenerationModel.ts``ImageCanvasSelectedLayerToolbarView.tsx``ImageCanvasContextMenusView.tsx``useImageCanvasGenerationWorkflow.ts``useImageCanvasGenerationSubmissionWorkflow.ts`
## 2026-08-05 game-chat Canvas 区间证明与 parent wake 持久收口
@@ -848,7 +848,7 @@
- 现象:用户点击图片素材的“快速编辑”后,画布上额外出现 `Quick Edit Generator` 占位,像是新建了一个生成器;但用户预期是在原图下方框选区域、填写一个提示词和模型,然后直接修改当前图。
- 原因:快速编辑入口和提交链路误用了 `createQuickEditGenerationDialogDraft(...)` / `CanvasGenerationDialogState`,把“覆盖源图”的快速编辑伪装成会产出新图层的生成器占位。
- 处理:图片快速编辑必须走 `QuickEditPanelState`,打开时归档当前 active generation dialog 但不创建新的 `mode="quick-edit"` dialog;提交时调用 `/api/editor/images/edits`,主来源始终使用当前图片已登记的 `resourceId``sourceAssetId`。带编号标注的图片上传后只作为辅助 `referenceImageSrcs`,不能替换主来源身份;成功后覆盖源图,失败时保留快速编辑面板。快速编辑任务进入 `generating` 后必须移除框选工具和覆盖层,禁止继续新增框选;失败恢复面板后可继续调整框选再重试。图片重绘、去背景、视频快速编辑等会产出新图层或异步占位的入口仍可走 generation dialog / placement 链路。
- 处理:图片快速编辑必须走 `QuickEditPanelState`,打开时归档当前 active generation dialog 但不创建新的 `mode="quick-edit"` dialog;提交时调用 `/api/editor/images/edits`,主来源始终使用当前图片已登记的 `resourceId``sourceAssetId`。带编号标注的图片上传后只作为辅助 `referenceImageSrcs`,不能替换主来源身份;成功后覆盖源图,失败时保留快速编辑面板。快速编辑任务进入 `generating` 后必须移除框选工具和覆盖层,禁止继续新增框选;失败恢复面板后可继续调整框选再重试。图片重绘、去背景等会产出新图层或异步占位的入口仍可走 generation dialog / placement 链路。
- 验证:`npm run test -- src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx -- --runInBand`,以及按需运行 `npm run test -- src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx -t "快速编辑|quick edit" -- --runInBand`
- 关联:`src/components/image-editor/ImageCanvasEditorView.tsx``src/components/image-editor/useImageCanvasGenerationWorkflow.ts``src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts``src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts``src/services/image-editor/editorImageReference.ts`
File diff suppressed because one or more lines are too long
@@ -205,7 +205,7 @@ controller 配置:
- `editor_icon_spritesheet_generation`:图标素材 spritesheet 生成和拆分。
- `editor_ui_design_asset_extraction`UI 设计图红框素材提取。
- `editor_character_animation_generation`:角色动作视频和帧素材生成。
- `editor_video_generation`:画布视频生成和视频素材快速编辑
- `editor_video_generation`:画布视频生成。
- `editor_sound_effect_generation` / `editor_background_music_generation`:画布音效与背景音乐。
画板结果的业务真相仍是 `asset_object``editor_project_resource`、账号级 `editor_asset`、可选 `asset_entity_binding` 和对应的 legacy / structured canvas 表;`editor_generation_operation` 只是提交回执,不替代这些 read model。worker 在 Provider 与 OSS 完成后只做 prepare:使用 owner + operation kind + job ID + stable slot 派生 resource/asset ID,构造可选 object/binding、候选 layout 和 compact job result,然后一次调用 `persist_editor_generation_result_and_return`。该 procedure 必须在当前事务快照校验 owner、job kind、由 `request_payload_json` 重算的 fingerprint 与未过期 lease,最后与业务记录一起完成 job 和 receipt。任一验证、binding 或 canvas CAS 失败都回滚全部数据库事实;worker 不得随后再调用 `complete_external_generation_job_and_return`。前端轮询单 job 到 completed 后重新读取项目快照,不从队列 payload、receipt 或本地临时响应重建正式图层。
@@ -113,10 +113,10 @@
- 占位图的生成器名称 / 原始尺寸、图片图层右上角素材类型标签、查看信息按钮和悬浮尺寸标签都按 viewport 反向缩放,画布缩小时保持屏幕可读尺寸。
- 查看信息按钮固定使用圆形 `i` 图标,不使用中括号、花括号或文本符号样式。
- 视频 / 角色 / 角色动作 / 音效 / 背景音乐待生成占位的角标同样按 viewport 反向缩放,不随画布缩放变小。
- 已生成角色图、角色动作图或其它生成结果图被点击时只选中图层并收起已有生成输入框;重绘、快速编辑和生成动画面板必须由对应工具栏按钮或右键菜单显式打开。快速编辑使用统一正向白名单,只支持普通静态图片、角色图、规范图、完整图标图集、UI 设计图、宣发图和视频单个拆分图标(backend reject)、角色动作 / 序列帧、音效与背景音乐不显示快速编辑入口,提交层也必须拒绝绕过入口的调用。
- 已生成角色图、角色动作图或其它生成结果图被点击时只选中图层并收起已有生成输入框;重绘、快速编辑和生成动画面板必须由对应工具栏按钮或右键菜单显式打开。快速编辑使用统一正向白名单,只支持普通静态图片、角色图、规范图、完整图标图集、图标规范、UI 设计图、宣发图和游戏场景图;视频单个拆分图标(backend reject)、角色动作 / 序列帧、音效与背景音乐不显示快速编辑入口,程序化打开和提交层也必须拒绝绕过入口的调用。
- 角色图层打开“生成动作”后再点击“改造”,必须重新打开角色形象生成器;动作生成对话框只把角色图层作为输入来源,不得被识别为该角色图层自身的来源生成器。
- 角色动作结果图层点击“改造”时,V2 必须通过 `references[id="source"]` 找回原角色图层,legacy 数据才允许以 `sourceResourceId` 回退;关联原角色已不存在时应显示明确提示,不得无响应或降级成图片生成器。
- `改造` 覆盖图片、规范、角色、图标、UI、宣发、游戏场景、视频、音效、背景音乐、角色动作和生成型图片编辑。有效 V2 只按 `action + fields[].id + references[].id` 恢复,引用只匹配当前画布图层;面板直接上传引用和已移出画布的引用不恢复。V2 不新增后续版本,读取时统一经 action 级 runtime decoder 原地收紧:已存在但未知、非法、已下线或与当前模型能力不兼容的参数统一回落到该 action 当前默认值,历史 Veo 也回落到当前默认视频模型;图片比例 / 尺寸按回落后的模型联动校验,角色动作帧数 / 时长按完整档位成对校验。发生参数回落时显示明确告警,再次提交和新快照只使用规范值并继续保存为 `version: 2`。服务端以实际媒体时长覆盖 V2 配方时必须保留 `fields[id="durationSeconds"]`,并写入归一后的有限数值,不能改写为无 `id` 的 legacy 展示字符串。可重新选择的引用缺失时打开面板、留空槽位并提示,提交门禁继续校验必填槽位;必须依赖原 `source` 图层才能构造面板的 action 也始终按 capability 保留改造入口,source 缺失时点击后显示不可替换原因并拒绝改造,运行期间来源变化时仍必须复检并拒绝。有效 V2 不得因引用缺失降级到 legacy。生成型图片编辑 V2 中已持久化的附加 `reference` 应恢复到可见参考槽,并让再次提交的模型、比例、尺寸、像素尺寸、参考图和新快照保持一致;普通快速编辑仍不得提交未展示的隐藏参考图。视频快速编辑必须把实际送入请求的源视频同步保存为 `references[id="videoReference"]`,不能只依赖 `sourceResourceId`;视频 V2 同步保存并恢复 `webSearchEnabled`。历史对话框和 legacy 数据保留 `assetKind/mediaType`、标题别名、资源尺寸 / 模型 / 时长 / `sourceResourceId` 回退,并对默认值恢复显示告警。V2 结构水合必须完整保留 `version/action`、字段与引用 `id`、有限数字、布尔值和无标签引用;一旦出现 `version``action` 却不满足 V2 合同,必须失败关闭,禁止降级成 legacy。
- `改造` 覆盖图片、规范、角色、图标、UI、宣发、游戏场景、视频、音效、背景音乐、角色动作和生成型图片编辑。有效 V2 只按 `action + fields[].id + references[].id` 恢复,引用只匹配当前画布图层;面板直接上传引用和已移出画布的引用不恢复。V2 不新增后续版本,读取时统一经 action 级 runtime decoder 原地收紧:已存在但未知、非法、已下线或与当前模型能力不兼容的参数统一回落到该 action 当前默认值,历史 Veo 也回落到当前默认视频模型;图片比例 / 尺寸按回落后的模型联动校验,角色动作帧数 / 时长按完整档位成对校验。发生参数回落时显示明确告警,再次提交和新快照只使用规范值并继续保存为 `version: 2`。服务端以实际媒体时长覆盖 V2 配方时必须保留 `fields[id="durationSeconds"]`,并写入归一后的有限数值,不能改写为无 `id` 的 legacy 展示字符串。可重新选择的引用缺失时打开面板、留空槽位并提示,提交门禁继续校验必填槽位;必须依赖原 `source` 图层才能构造面板的 action 也始终按 capability 保留改造入口,source 缺失时点击后显示不可替换原因并拒绝改造,运行期间来源变化时仍必须复检并拒绝。有效 V2 不得因引用缺失降级到 legacy。生成型图片编辑 V2 中已持久化的附加 `reference` 应恢复到可见参考槽,并让再次提交的模型、比例、尺寸、像素尺寸、参考图和新快照保持一致;普通快速编辑仍不得提交未展示的隐藏参考图。普通视频生成继续按现有 V2 合同保存并恢复 `references[id="videoReference"]` `webSearchEnabled`,该合同不表示支持视频快速编辑。历史对话框和 legacy 数据保留 `assetKind/mediaType`、标题别名、资源尺寸 / 模型 / 时长 / `sourceResourceId` 回退,并对默认值恢复显示告警。V2 结构水合必须完整保留 `version/action`、字段与引用 `id`、有限数字、布尔值和无标签引用;一旦出现 `version``action` 却不满足 V2 合同,必须失败关闭,禁止降级成 legacy。
- 配方元数据不等于改造 capability。完美像素、手动去背景、裁扩、手动图集拆分以及图标 / UI 自动切片分别保存 `image.perfect-pixel``image.remove-background``image.crop-expand``spritesheet.split`,统一使用 `fields: []`;有正式来源行时只保留不可编辑的 `references[id="source"]` 权威来源,没有正式行时保留空引用。这四个确定性 action 永不显示或执行“改造”,历史 `pixel-art-snap-*` 结果也按 task identity 拒绝改造。整张生成图集仍保留原生成 action;生成任务内部自动抠图仍属于同源后处理,不提升为独立用户 action。
- 任何会移除画布图层的入口,包括删除、右键剪切和素材库删除关联素材,都必须同步清理该图层关联的生成面板和派生状态,不得在保存或刷新后恢复成孤立占位。
@@ -371,6 +371,11 @@ describe('ImageCanvasContextMenusView', () => {
mediaType: 'audio' as const,
assetKind: 'background-music' as const,
},
{
title: '视频',
mediaType: 'video' as const,
assetKind: 'video' as const,
},
])('hides quick edit from both menus for $title', (overrides) => {
const layer = createLayer(overrides);
renderContextMenus({
@@ -964,9 +964,16 @@ describe('ImageCanvasGenerationModel', () => {
isQuickEditSupportedLayer({
...buildSourceLayer(),
mediaType: 'video',
assetKind: null,
}),
).toBe(false);
expect(
isQuickEditSupportedLayer({
...buildSourceLayer(),
mediaType: 'image',
assetKind: 'video',
}),
).toBe(true);
).toBe(false);
});
it('adds reference image semantics and snapshots for spec generation references', () => {
@@ -712,7 +712,6 @@ export function buildQuickEditModelOptions(currentModel: string) {
const QUICK_EDIT_SUPPORTED_MEDIA_TYPES = new Set<CanvasLayer['mediaType']>([
undefined,
'image',
'video',
]);
const QUICK_EDIT_SUPPORTED_ASSET_KINDS = new Set<CanvasLayer['assetKind']>([
@@ -725,7 +724,6 @@ const QUICK_EDIT_SUPPORTED_ASSET_KINDS = new Set<CanvasLayer['assetKind']>([
'publication-material',
'ui-design',
'scene',
'video',
]);
export function isQuickEditSupportedLayer(layer: CanvasLayer) {
@@ -13,23 +13,6 @@ import type {
} from './ImageCanvasEditorTypes';
import { ImageCanvasQuickEditPanelView } from './ImageCanvasQuickEditPanelView';
function createLayer(): CanvasLayer {
return {
id: 'layer-a',
resourceId: 'resource-a',
title: '源图',
src: 'data:image/png;base64,c291cmNl',
x: 0,
y: 0,
width: 320,
height: 180,
originalWidth: 320,
originalHeight: 180,
zIndex: 1,
sourceType: 'uploaded',
};
}
function QuickEditPanelHarness({
initialPanel,
onSubmit = vi.fn(),
@@ -45,12 +28,27 @@ function QuickEditPanelHarness({
const [isReferenceMenuOpen, setIsReferenceMenuOpen] = useState(false);
const [isPickingReference, setIsPickingReference] = useState(false);
const referenceButtonRef = useRef<HTMLButtonElement | null>(null);
const sourceLayer: CanvasLayer = {
id: 'layer-a',
resourceId: 'resource-a',
title: '源图片',
src: '/source.png',
mediaType: 'image',
originalWidth: 1024,
originalHeight: 1024,
width: 512,
height: 512,
x: 0,
y: 0,
zIndex: 1,
sourceType: 'uploaded',
};
return panel ? (
<div>
<ImageCanvasQuickEditPanelView
panel={panel}
sourceLayer={createLayer()}
sourceLayer={sourceLayer}
style={{ left: 10, top: 20 }}
referenceButtonRef={referenceButtonRef}
isReferenceMenuOpen={isReferenceMenuOpen}
@@ -51,8 +51,9 @@ function resetFailedPanelStatus<
function createQuickEditDialog(
panel: QuickEditPanelState,
): GenerateDialogState {
const isQuickEdit = (panel.mode ?? 'quick-edit') === 'quick-edit';
return {
mode: 'quick-edit',
mode: isQuickEdit ? 'quick-edit' : 'generate',
prompt: panel.prompt,
status: panel.status,
errorMessage: panel.errorMessage,
@@ -106,11 +107,16 @@ export function ImageCanvasQuickEditPanelView({
hasPendingImageReferenceUploads = false,
onSubmit,
}: ImageCanvasQuickEditPanelViewProps) {
const isImageQuickEdit = sourceLayer.mediaType !== 'video';
const isRedraw = panel.mode === 'redraw';
const referenceCount = panel.quickEditReferences?.length ?? 0;
const canAddReferences =
!isRedraw &&
referenceCount <
resolveExtraImageReferenceLimit(panel.model, QUICK_EDIT_REFERENCE_LIMIT, 1);
resolveExtraImageReferenceLimit(
panel.model,
QUICK_EDIT_REFERENCE_LIMIT,
1,
);
const quickEditDialog = createQuickEditDialog(panel);
return (
@@ -147,19 +153,21 @@ export function ImageCanvasQuickEditPanelView({
onRememberImageModel={onRememberImageModel}
hasPendingImageReferenceUploads={hasPendingImageReferenceUploads}
onSubmit={onSubmit}
dialogLabel="快速编辑图片"
includeDimensions={isImageQuickEdit}
dialogLabel={isRedraw ? '重绘图片' : '快速编辑图片'}
includeDimensions={true}
includeModel={true}
includeReferences={false}
promptLabel="快速编辑提示词"
promptPlaceholder="你希望素材如何修改?"
optionLabelPrefix="快速编辑"
promptLabel={isRedraw ? '重绘提示词' : '快速编辑提示词'}
promptPlaceholder={
isRedraw ? '把画面重绘成什么样?' : '你希望素材如何修改?'
}
optionLabelPrefix={isRedraw ? '重绘' : '快速编辑'}
dimensionRatioAriaLabelPrefix="比例"
dimensionSizeAriaLabelPrefix="尺寸"
referenceUploadTarget="quick-edit-reference"
referenceButtonLabel="添加快速编辑参考图"
referenceButtonAriaLabel="添加快速编辑参考图"
referenceMenuLabel="快速编辑参考图来源"
referenceButtonLabel={isRedraw ? '' : '添加快速编辑参考图'}
referenceButtonAriaLabel={isRedraw ? undefined : '添加快速编辑参考图'}
referenceMenuLabel={isRedraw ? '' : '快速编辑参考图来源'}
referenceSlotTone="quick-edit"
referenceSlotDisabled={!canAddReferences}
showReferenceAddButton={false}
@@ -171,9 +179,9 @@ export function ImageCanvasQuickEditPanelView({
promptClassName="image-canvas-editor__generation-prompt image-canvas-editor__quick-edit-prompt"
referenceSlotClassName="image-canvas-editor__quick-edit-reference"
submitButtonClassName="image-canvas-editor__generation-submit image-canvas-editor__quick-edit-submit"
submitLabel="修改"
submitAriaLabel="修改"
submittingStatusLabel="修改中"
submitLabel={isRedraw ? '生成' : '修改'}
submitAriaLabel={isRedraw ? '生成' : '修改'}
submittingStatusLabel={isRedraw ? '生成中' : '修改中'}
renderEditorPortal={renderEditorPortal}
buildPortalMenuStyle={buildPortalMenuStyle}
/>
@@ -444,7 +444,7 @@ describe('ImageCanvasSelectedLayerToolbarView', () => {
within(videoToolbar)
.getAllByRole('button')
.map((button) => button.getAttribute('aria-label')),
).toEqual(['快速编辑', '改造', '下载按钮']);
).toEqual(['改造', '下载按钮']);
cleanup();
renderSelectedToolbar({
@@ -337,14 +337,12 @@ function SubmissionWorkflowHarness({
quickEditPanel,
quickEditSourceLayer,
quickEditSelectionState: initialQuickEditSelectionState,
canvasGenerationDialogs: dialogs.canvasGenerationDialogs,
setQuickEditPanel,
characterAnimationPanel,
characterAnimationDialog: activeCanvasDialog,
characterAnimationSourceLayer,
setCharacterAnimationPanel,
setGenerateDialog: dialogs.setGenerateDialog,
openCanvasGenerationDialog: dialogs.openCanvasGenerationDialog,
updateCanvasGenerationDialogById: dialogs.updateCanvasGenerationDialogById,
hasCanvasGenerationDialogById: dialogs.hasCanvasGenerationDialogById,
getCanvasGenerationDialogById: dialogs.getCanvasGenerationDialogById,
@@ -1496,47 +1494,15 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
expect(refreshWalletBalance).toHaveBeenCalledTimes(1);
});
it('submits video quick edits through the video API with the source video', async () => {
const applyProjectSnapshot = vi.fn();
const upsertGeneratedAsset = vi.fn();
generateEditorVideoMock.mockResolvedValueOnce(
createVideoGenerated({
prompt: '把当前图改成雨天',
project: {
projectId: 'editor-project-1',
title: '项目',
coverImageSrc: null,
viewport: { x: 0, y: 0, scale: 1 },
layers: [],
resources: [],
createdAt: '2026-06-27T00:00:00.000Z',
updatedAt: '2026-06-27T00:00:00.000Z',
},
asset: {
assetId: 'asset-video-quick-edit',
folderId: 'user-1:asset-folder:project',
label: '源视频 快速编辑',
imageSrc: 'https://assets.example.test/generated-video.mp4',
thumbnailSrc: 'https://assets.example.test/generated-video-cover.png',
width: 854,
height: 480,
sourceType: 'generated',
assetKind: 'video',
},
}),
);
it('rejects direct video quick-edit submission without calling generation APIs', () => {
render(
<SubmissionWorkflowHarness
projectId="editor-project-1"
applyProjectSnapshot={applyProjectSnapshot}
upsertGeneratedAsset={upsertGeneratedAsset}
initialLayers={[
createLayer({
title: '源视频',
src: 'data:video/mp4;base64,c291cmNlLXZpZGVv',
src: 'https://assets.example.test/source-video.mp4',
mediaType: 'video',
assetKind: 'video',
model: 'gpt-image-2',
originalWidth: 854,
originalHeight: 480,
width: 427,
@@ -1549,66 +1515,19 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
size: '854x480',
model: 'gpt-image-2',
status: 'idle',
quickEditReferences: [
{
id: 'ref-style',
label: '风格参考',
src: 'https://assets.example.test/style.png',
},
],
}}
/>,
);
fireEvent.click(screen.getByRole('button', { name: '提交快速编辑' }));
await waitFor(() => {
expect(generateEditorVideoMock).toHaveBeenCalledWith(
expect.objectContaining({
prompt: '把当前图改成雨天',
model: 'seedance2.0-fast',
aspectRatio: '16:9',
durationSeconds: 4,
resolution: '480p',
mode: 'std',
sound: 'off',
referenceVideoSrcs: [
'generated-character-drafts/editor/generation-references/reference.png',
],
generationInputs: expect.objectContaining({
version: 2,
action: 'video.generate',
references: [
{
id: 'videoReference',
title: '参考视频 1',
label: '源视频',
refType: 'project-resource',
refId: 'resource-source',
},
],
}),
assetFolderId: 'project',
assetLabel: '源视频 快速编辑',
}),
);
});
expect(generateEditorVideoMock.mock.calls[0]?.[0]).not.toHaveProperty(
'referenceImageSrcs',
);
expect(editEditorImageMock).not.toHaveBeenCalled();
expect(generateEditorImageMock).not.toHaveBeenCalled();
expect(generateEditorCharacterAnimationMock).not.toHaveBeenCalled();
await waitFor(() => {
expect(applyProjectSnapshot).toHaveBeenCalledWith(
expect.objectContaining({ projectId: 'editor-project-1' }),
);
expect(upsertGeneratedAsset).toHaveBeenCalledWith(
expect.objectContaining({
assetId: 'asset-video-quick-edit',
thumbnailSrc: 'https://assets.example.test/generated-video-cover.png',
}),
);
});
expect(generateEditorVideoMock).not.toHaveBeenCalled();
expect(screen.getByTestId('quick-edit').textContent).toContain(
'failed:把当前图改成雨天:当前素材类型不支持快速编辑',
);
});
it('uploads inline video references and keeps image objectKey and asset URLs', async () => {
File diff suppressed because it is too large Load Diff
@@ -1493,6 +1493,28 @@ describe('useImageCanvasGenerationWorkflow', () => {
);
});
it('does not open quick edit programmatically for video layers', () => {
render(
<GenerationWorkflowHarness
initialLayers={[
createLayer({
title: '源视频',
src: '/generated/video.mp4',
mediaType: 'video',
assetKind: 'video',
}),
]}
/>,
);
fireEvent.click(screen.getByRole('button', { name: '打开快速编辑' }));
expect(screen.getByTestId('tool').textContent).toBe('select');
expect(screen.getByTestId('selected').textContent).toBe('-');
expect(screen.getByTestId('quick-edit').textContent).toBe('-');
expect(screen.getByTestId('quick-edit-selection').textContent).toBe('-');
});
it('calculates character animation button price from the model pricing config', () => {
render(<GenerationWorkflowHarness />);
@@ -3829,14 +3829,12 @@ export function useImageCanvasGenerationWorkflow({
quickEditPanel,
quickEditSourceLayer,
quickEditSelectionState,
canvasGenerationDialogs,
setQuickEditPanel,
characterAnimationPanel: effectiveCharacterAnimationPanel,
characterAnimationDialog,
characterAnimationSourceLayer,
setCharacterAnimationPanel,
setGenerateDialog,
openCanvasGenerationDialog,
updateCanvasGenerationDialogById,
hasCanvasGenerationDialogById,
getCanvasGenerationDialogById,