合并最新 master 到 feat/five_min_design,并修复 master 的 Windows 构建中断
Project CI / Repository checks (pull_request) Successful in 1m15s
Project CI / Frontend tests (pull_request) Successful in 2m55s
Project CI / Backend tests (pull_request) Successful in 3m58s
Project CI / Native shell tests (pull_request) Failing after 10m29s

master 侧 5 个提交(8e78be766..9f5c84ee7)。仅一处冲突:
src/view/home/index.tsx 的 react import——我们这侧对该文件只有 prettier 格式化
改动,master 新增的 useRef 在合并后正文里用了 2 次,取 master 那行。

修复 master 带来的 Windows 构建中断(E0658):
project/manifest.rs 的 #[cfg(windows)] 分支用了 std 未稳定 API
`MetadataExt::number_of_links`(rust-lang#63010),而 rust-toolchain.toml 锁在
stable 1.96.0。该文件与 origin/master 逐字节相同,即 master 自身在 Windows 上就
构建不过——Linux CI 上 #[cfg(windows)] 整块不参与编译,所以 CI 全绿。
改为本仓库既有写法:自声明 ByHandleFileInformation 调 GetFileInformationByHandle
(另见 runner/endpoint.rs、tool_plan_handoff/storage_windows.rs 等六处)。保留原
错误文案与 fail-closed 语义(取不到句柄信息与确实是硬链接同等拒绝),并按
endpoint.rs 先例一并拒绝 directory / reparse point。
该修复目前只在本分支,须回流 master,否则下次合并会再撞一次。踩坑记录见
pitfalls.md 2026-08-15 条。

验证:
- cargo check --offline --all-targets 通过(修复前 E0658,修复后 Finished)
- cargo fmt --check 通过
- 定向 Rust 测试 project::manifest / godot / static_delegate /
  collaboration::static_deliveries 65 passed / 0 failed
- agc:typecheck 通过;check:encoding 通过(5376 files);git diff --check 干净
- 前端 vitest apps/ai-game-creator-shell/tests:738 passed / 1 failed,唯一失败是
  已知的 Windows symlink EPERM(agentSwarmTestEntry),非本次回归

两条既有环境失败,已核实与本次合并无关:
- command_exec::tests 两条报「找不到受信任的 rg 可执行文件」,该文件相对合并基线
  逐字节相同
- npm run check:native-shells 在合并前的 master worktree 上失败得一模一样
  (spawnSync npm.cmd EINVAL,脚本 spawn npm.cmd 未带 shell: true)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 05:37:00 +00:00
54 changed files with 2799 additions and 1066 deletions
@@ -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({
@@ -1795,7 +1795,6 @@ export function ImageCanvasGenerationComposerView({
{quickEditPanel && quickEditSourceLayer && quickEditPanelStyle ? (
<ImageCanvasQuickEditPanelView
panel={quickEditPanel}
sourceLayer={quickEditSourceLayer}
style={quickEditPanelStyle}
referenceButtonRef={generationReferenceButtonRef}
isReferenceMenuOpen={isGenerationReferenceMenuOpen}
@@ -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) {
@@ -424,6 +424,30 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
});
});
it('rejects persisted video quick-edit dialogs during submission planning', () => {
const sourceLayer = createLayer({
title: '历史视频',
src: 'https://assets.example.test/history.mp4',
mediaType: 'video',
assetKind: 'video',
originalWidth: 854,
originalHeight: 480,
});
expect(() =>
buildImageGenerationSubmissionPlan({
dialog: {
mode: 'quick-edit',
prompt: '把当前画面改成雨天',
status: 'idle',
sourceLayerId: sourceLayer.id,
},
layers: [sourceLayer],
nextGeneratedIndex: 4,
}),
).toThrow('当前素材类型不支持快速编辑');
});
it('does not send image style from an incomplete quick-edit snapshot', () => {
const plan = buildImageGenerationSubmissionPlan({
dialog: {
@@ -54,6 +54,7 @@ import {
IMAGE_MODEL_GPT_IMAGE_2,
inferEditorImageAspectRatio,
inferEditorImageSizeLabel,
isQuickEditSupportedLayer,
normalizeEditorImageModel,
resolveCharacterAnimationSourceImageSrc,
resolveEditorImageGenerationPixelSize,
@@ -577,6 +578,9 @@ export function buildImageGenerationSubmissionPlan({
if (!sourceLayer) {
throw new Error('未找到要改造的原图');
}
if (!isQuickEditSupportedLayer(sourceLayer)) {
throw new Error('当前素材类型不支持快速编辑');
}
const basePrompt = dialog.prompt.trim() || '快速编辑图片';
const normalizedQuickEditPrompt = basePrompt;
const requestedImageModel = normalizeEditorImageModel(
@@ -6,30 +6,12 @@ import { describe, expect, it, vi } from 'vitest';
import { setPlainTextEditorValue } from '../common/AutoGrowTextArea.test-utils';
import type {
CanvasLayer,
CharacterReferenceImage,
QuickEditPanelState,
UploadTarget,
} 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(),
@@ -50,7 +32,6 @@ function QuickEditPanelHarness({
<div>
<ImageCanvasQuickEditPanelView
panel={panel}
sourceLayer={createLayer()}
style={{ left: 10, top: 20 }}
referenceButtonRef={referenceButtonRef}
isReferenceMenuOpen={isReferenceMenuOpen}
@@ -8,7 +8,6 @@ import {
import { ImageCanvasBasicGenerationComposerView } from './ImageCanvasBasicGenerationComposerView';
import type {
CanvasLayer,
GenerateDialogState,
QuickEditPanelState,
UploadTarget,
@@ -20,7 +19,6 @@ import {
export type ImageCanvasQuickEditPanelViewProps = {
panel: QuickEditPanelState;
sourceLayer: CanvasLayer;
style: CSSProperties;
referenceButtonRef?: RefObject<HTMLButtonElement | null>;
isReferenceMenuOpen?: boolean;
@@ -92,7 +90,6 @@ function syncPanelFromDialogUpdate(
export function ImageCanvasQuickEditPanelView({
panel,
sourceLayer,
style,
referenceButtonRef,
isReferenceMenuOpen = false,
@@ -106,7 +103,6 @@ export function ImageCanvasQuickEditPanelView({
hasPendingImageReferenceUploads = false,
onSubmit,
}: ImageCanvasQuickEditPanelViewProps) {
const isImageQuickEdit = sourceLayer.mediaType !== 'video';
const referenceCount = panel.quickEditReferences?.length ?? 0;
const canAddReferences =
referenceCount <
@@ -148,7 +144,7 @@ export function ImageCanvasQuickEditPanelView({
hasPendingImageReferenceUploads={hasPendingImageReferenceUploads}
onSubmit={onSubmit}
dialogLabel="快速编辑图片"
includeDimensions={isImageQuickEdit}
includeDimensions={true}
includeModel={true}
includeReferences={false}
promptLabel="快速编辑提示词"
@@ -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,58 @@ 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();
expect(generateEditorVideoMock).not.toHaveBeenCalled();
expect(screen.getByTestId('quick-edit').textContent).toContain(
'failed:把当前图改成雨天:当前素材类型不支持快速编辑',
);
});
it('rejects a hydrated video quick-edit dialog before calling generation APIs', async () => {
render(
<SubmissionWorkflowHarness
initialLayers={[
createLayer({
title: '历史视频',
src: 'https://assets.example.test/history.mp4',
mediaType: 'video',
assetKind: 'video',
originalWidth: 854,
originalHeight: 480,
width: 427,
height: 240,
}),
]}
initialDialog={{
id: 'generation-dialog-hydrated-video-quick-edit',
mode: 'quick-edit',
prompt: '把当前画面改成雨天',
status: 'idle',
composerOpen: true,
sourceLayerId: 'layer-source',
}}
/>,
);
fireEvent.click(screen.getByRole('button', { name: '设置初始对话' }));
fireEvent.click(screen.getByRole('button', { name: '提交当前生成' }));
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(screen.getByTestId('tracked-dialog').textContent).toContain(
'quick-edit:把当前画面改成雨天:failed:open:当前素材类型不支持快速编辑',
);
});
expect(editEditorImageMock).not.toHaveBeenCalled();
expect(generateEditorImageMock).not.toHaveBeenCalled();
expect(generateEditorVideoMock).not.toHaveBeenCalled();
});
it('uploads inline video references and keeps image objectKey and asset URLs', async () => {
@@ -50,7 +50,6 @@ import type {
QuickEditPanelState,
SidebarPanel,
} from './ImageCanvasEditorTypes';
import { createQuickEditGenerationDialogDraft } from './ImageCanvasGenerationDialogModel';
import {
applyImageEditResultToSourceLayer,
createAudioResultLayer,
@@ -63,12 +62,9 @@ import {
buildCharacterAnimationGenerationInputs,
buildQuickEditGenerationInputs,
buildUiDesignAssetExtractionGenerationInputs,
buildVideoGenerationInputs,
createCanvasLayerReference,
DEFAULT_EDITOR_BGFILTER_SEG_MODEL,
DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR,
DEFAULT_IMAGE_MODEL,
DEFAULT_VIDEO_MODEL,
inferEditorImageAspectRatio,
inferEditorImageSizeLabel,
isCanvasGenerationDialog,
@@ -77,7 +73,6 @@ import {
resolveEditorImageGenerationPixelSize,
resolveImageGenerationErrorMessage,
} from './ImageCanvasGenerationModel';
import { chooseGenerationPlacement } from './ImageCanvasGenerationPlacementModel';
import {
buildCharacterAnimationSubmissionPlan,
buildIconSpritesheetGenerationSubmissionPlan,
@@ -338,7 +333,6 @@ type GenerationSubmissionWorkflowOptions = {
quickEditPanel: QuickEditPanelState | null;
quickEditSourceLayer: CanvasLayer | null;
quickEditSelectionState: UiAssetExtractionState | null;
canvasGenerationDialogs: CanvasGenerationDialogState[];
setQuickEditPanel: Dispatch<SetStateAction<QuickEditPanelState | null>>;
characterAnimationPanel: CharacterAnimationPanelState | null;
characterAnimationDialog: CanvasGenerationDialogState | null;
@@ -347,9 +341,6 @@ type GenerationSubmissionWorkflowOptions = {
SetStateAction<CharacterAnimationPanelState | null>
>;
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
openCanvasGenerationDialog: (
dialog: Omit<CanvasGenerationDialogState, 'id'>,
) => string;
updateCanvasGenerationDialogById: (
dialogId: string,
updater: CanvasGenerationDialogUpdater,
@@ -650,10 +641,6 @@ function buildRightSideCanvasCompletionPlaceholder(
};
}
function isVideoQuickEditSource(layer: CanvasLayer) {
return layer.mediaType === 'video' || layer.assetKind === 'video';
}
export function useImageCanvasGenerationSubmissionWorkflow({
layers,
canvasSize,
@@ -662,14 +649,12 @@ export function useImageCanvasGenerationSubmissionWorkflow({
quickEditPanel,
quickEditSourceLayer,
quickEditSelectionState,
canvasGenerationDialogs,
setQuickEditPanel,
characterAnimationPanel,
characterAnimationDialog,
characterAnimationSourceLayer,
setCharacterAnimationPanel,
setGenerateDialog,
openCanvasGenerationDialog,
updateCanvasGenerationDialogById,
hasCanvasGenerationDialogById,
getCanvasGenerationDialogById,
@@ -1451,63 +1436,22 @@ export function useImageCanvasGenerationSubmissionWorkflow({
const normalizedQuickEditModel = normalizeEditorImageModel(
quickEditPanel.model,
);
const quickEditOutputSize = isVideoQuickEditSource(quickEditSourceLayer)
? {
width: quickEditSourceLayer.originalWidth,
height: quickEditSourceLayer.originalHeight,
}
: resolveEditorImageGenerationPixelSize({
model: normalizedQuickEditModel,
aspectRatio: quickEditAspectRatio,
imageSize: quickEditImageSize,
});
const quickEditOutputSize = resolveEditorImageGenerationPixelSize({
model: normalizedQuickEditModel,
aspectRatio: quickEditAspectRatio,
imageSize: quickEditImageSize,
});
const quickEditSize = `${quickEditOutputSize.width}x${quickEditOutputSize.height}`;
const normalizedPrompt = quickEditPanel.prompt.trim() || '快速编辑图片';
let quickEditDialogId: string | undefined;
const shouldUseCanvasPlaceholder =
isVideoQuickEditSource(quickEditSourceLayer);
if (shouldUseCanvasPlaceholder) {
const quickEditDraft = createQuickEditGenerationDialogDraft({
sourceLayer: quickEditSourceLayer,
prompt: normalizedPrompt,
status: 'generating',
references: quickEditReferences,
model: quickEditPanel.model,
aspectRatio: quickEditAspectRatio,
imageSize: quickEditImageSize,
frame: {
width: quickEditOutputSize.width,
height: quickEditOutputSize.height,
},
assetLabel: quickEditResultTitle,
});
const quickEditPlacement = quickEditDraft.placeholder
? chooseGenerationPlacement({
canvasSize,
viewport,
frame: quickEditDraft.placeholder,
layers,
generationDialogs: canvasGenerationDialogs,
})
: quickEditDraft.placeholder;
quickEditDialogId = openCanvasGenerationDialog({
...quickEditDraft,
// 中文注释:视频快速编辑仍落到独立视频结果占位,图片快速编辑直接替换源图。
placeholder: quickEditPlacement,
});
setQuickEditPanel(null);
selectSingleLayer(null);
} else {
setQuickEditPanel({
...quickEditPanel,
aspectRatio: quickEditAspectRatio,
imageSize: quickEditImageSize,
size: quickEditSize,
prompt: normalizedPrompt,
status: 'generating',
errorMessage: undefined,
});
}
setQuickEditPanel({
...quickEditPanel,
aspectRatio: quickEditAspectRatio,
imageSize: quickEditImageSize,
size: quickEditSize,
prompt: normalizedPrompt,
status: 'generating',
errorMessage: undefined,
});
try {
const extraReferenceImageSrcs = await Promise.all(
@@ -1515,105 +1459,6 @@ export function useImageCanvasGenerationSubmissionWorkflow({
resolveEditorGenerationMediaReference(reference, 'image', projectId),
),
);
const quickEditFrame = quickEditDialogId
? getGeneratingDialogPlaceholder({
id: quickEditDialogId,
mode: 'quick-edit',
prompt: normalizedPrompt,
status: 'generating',
})
: undefined;
const quickEditCanvasCompletionPlaceholder =
quickEditFrame ??
buildRightSideCanvasCompletionPlaceholder(quickEditSourceLayer, {
width: quickEditOutputSize.width,
height: quickEditOutputSize.height,
originalWidth: quickEditOutputSize.width,
originalHeight: quickEditOutputSize.height,
});
if (isVideoQuickEditSource(quickEditSourceLayer)) {
const videoModel = DEFAULT_VIDEO_MODEL;
const videoResolution = '480p' as const;
const videoDurationSeconds = 4 as const;
const sourceVideoSrc = await resolveEditorGenerationMediaReference(
quickEditSourceLayer,
'video',
projectId,
{ allowRegisteredIds: false },
);
const generationInputs = buildVideoGenerationInputs(
normalizedPrompt,
[createCanvasLayerReference(quickEditSourceLayer)],
{
model: videoModel,
aspectRatio: '16:9',
resolution: videoResolution,
durationSeconds: videoDurationSeconds,
sound: 'off',
},
);
const generated = await runEditorGenerationWithWalletRefresh(
generateEditorVideo({
prompt: normalizedPrompt,
model: videoModel,
aspectRatio: '16:9',
durationSeconds: videoDurationSeconds,
resolution: videoResolution,
mode: 'std',
sound: 'off',
...(extraReferenceImageSrcs.length
? { referenceImageSrcs: extraReferenceImageSrcs }
: {}),
referenceVideoSrcs: [sourceVideoSrc],
projectId,
generationInputs,
sourceResourceId: quickEditSourceLayer.resourceId,
assetKind: quickEditSourceLayer.assetKind,
assetFolderId,
assetLabel: quickEditResultTitle,
...(projectId && quickEditCanvasCompletionPlaceholder
? {
canvasCompletion: {
dialogId: quickEditDialogId,
title: quickEditResultTitle,
placeholder: quickEditCanvasCompletionPlaceholder,
},
}
: {}),
}),
onWalletBalanceMayHaveChanged,
);
if (
await applyQueuedEditorGenerationProject(
generated,
projectId,
applyProjectSnapshot,
onQueuedGenerationTask,
onWalletBalanceMayHaveChanged,
)
) {
return;
}
if (generated.project && applyProjectSnapshot) {
applyProjectSnapshot(generated.project);
if (generated.asset) {
upsertGeneratedAsset?.(generated.asset);
}
return;
}
addVideoResultLayer(
generated,
quickEditResultTitle,
generationInputs,
quickEditFrame,
quickEditDialogId,
{
sourceLayer: quickEditSourceLayer,
assetKind: quickEditSourceLayer.assetKind,
},
);
return;
}
const quickEditSelectionMarks =
quickEditSelectionState?.sourceLayerId === quickEditSourceLayer.id
? quickEditSelectionState.marks
@@ -1700,22 +1545,10 @@ export function useImageCanvasGenerationSubmissionWorkflow({
generated,
quickEditSourceLayer,
generationInputs,
quickEditDialogId,
undefined,
quickEditResultTitle,
);
} catch (error) {
if (quickEditDialogId) {
updateCanvasGenerationDialogById(
quickEditDialogId,
(currentDialog) => ({
...currentDialog,
prompt: normalizedPrompt,
status: 'failed',
composerOpen: false,
errorMessage: resolveImageGenerationErrorMessage(error),
}),
);
}
setQuickEditPanel({
...quickEditPanel,
aspectRatio: quickEditAspectRatio,
@@ -1727,26 +1560,16 @@ export function useImageCanvasGenerationSubmissionWorkflow({
});
}
}, [
addVideoResultLayer,
applyQuickEditResultToSourceLayer,
applyProjectSnapshot,
canvasGenerationDialogs,
canvasSize,
getGeneratingDialogPlaceholder,
layers,
openCanvasGenerationDialog,
projectId,
assetFolderId,
upsertGeneratedAsset,
quickEditPanel,
quickEditSourceLayer,
quickEditSelectionState,
selectSingleLayer,
setActiveSidebarPanel,
setActiveTool,
setQuickEditPanel,
updateCanvasGenerationDialogById,
viewport,
onGenerationWarning,
onQueuedGenerationTask,
onWalletBalanceMayHaveChanged,
@@ -1558,6 +1558,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 />);
@@ -3871,14 +3871,12 @@ export function useImageCanvasGenerationWorkflow({
quickEditPanel,
quickEditSourceLayer,
quickEditSelectionState,
canvasGenerationDialogs,
setQuickEditPanel,
characterAnimationPanel: effectiveCharacterAnimationPanel,
characterAnimationDialog,
characterAnimationSourceLayer,
setCharacterAnimationPanel,
setGenerateDialog,
openCanvasGenerationDialog,
updateCanvasGenerationDialogById,
hasCanvasGenerationDialogById,
getCanvasGenerationDialogById,