停止向用户泄露抠图模型 (#120)
Project CI / Repository checks (push) Successful in 1m19s
Project CI / Frontend tests (push) Successful in 3m40s
Project CI / Backend tests (push) Successful in 4m11s
Project CI / Native shell tests (push) Successful in 12m36s

Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/120
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
This commit was merged in pull request #120.
This commit is contained in:
2026-07-31 16:40:43 +08:00
committed by 段舒康
parent 48f9c8e690
commit 72acd0ec05
29 changed files with 1839 additions and 215 deletions
@@ -171,7 +171,6 @@ function getAssetHaystack(asset: EditorAssetSnapshot) {
asset.prompt,
asset.actualPrompt,
asset.model,
asset.provider,
asset.sourceType,
asset.assetKind,
...(asset.generationInputs?.fields ?? []).flatMap((field) => [
@@ -246,7 +245,6 @@ function projectResourceToShowcaseAsset(
prompt: resource.prompt ?? null,
actualPrompt: resource.actualPrompt ?? null,
model: resource.model ?? null,
provider: resource.provider ?? null,
taskId: resource.taskId ?? null,
durationSeconds: resource.durationSeconds,
sourceResourceId: resource.sourceResourceId,
@@ -9,6 +9,8 @@ import {
filterGroupedAssetsBySearch,
getSelectableAssets,
groupAssetsByFolder,
layerMatchesSearch,
layerStartsWithSearch,
moveAssetMarqueeToPointer,
moveAssetToFolderLocally,
removeAssetById,
@@ -28,6 +30,7 @@ import {
toggleAssetSelection,
} from './ImageCanvasAssetLibraryModel';
import type {
CanvasLayer,
EditorAsset,
EditorAssetFolder,
} from './ImageCanvasEditorTypes';
@@ -60,6 +63,24 @@ function createAsset(overrides: Partial<EditorAsset> = {}): EditorAsset {
};
}
function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
return {
id: 'layer-a',
resourceId: 'resource-a',
title: '图层A',
src: 'data:image/png;base64,YQ==',
x: 0,
y: 0,
width: 320,
height: 240,
originalWidth: 320,
originalHeight: 240,
zIndex: 1,
sourceType: 'generated',
...overrides,
};
}
describe('ImageCanvasAssetLibraryModel', () => {
it('groups assets by folder and resolves selectable uploaded assets', () => {
const folders = [
@@ -184,6 +205,60 @@ describe('ImageCanvasAssetLibraryModel', () => {
).toEqual([{ id: 'folder-uploaded', assets: ['asset-b', 'asset-c'] }]);
});
it('excludes internal processing models and providers from asset and layer search without deleting metadata', () => {
const internalAsset = createAsset({
id: 'asset-internal',
label: '透明角色',
sourceType: 'generated',
model: 'BgFilter complex',
provider: 'BgFilter',
});
const visibleAsset = createAsset({
id: 'asset-visible',
label: '生成角色',
sourceType: 'generated',
model: 'gpt-image-2',
provider: 'VectorEngine',
});
const groupedAssets = groupAssetsByFolder(
[createFolder()],
[internalAsset, visibleAsset],
);
const internalLayer = createLayer({
model: 'segment-common-image',
provider: 'Aliyun Matting',
});
const visibleLayer = createLayer({
id: 'layer-visible',
model: 'audio1.0',
provider: 'VectorEngine',
});
expect(filterGroupedAssetsBySearch(groupedAssets, 'bgfilter')).toEqual([]);
expect(filterGroupedAssetsBySearch(groupedAssets, 'vectorengine')).toEqual(
[],
);
expect(
filterGroupedAssetsBySearch(groupedAssets, 'gpt-image-2')[0]?.assets.map(
(asset) => asset.id,
),
).toEqual(['asset-visible']);
expect(layerMatchesSearch(internalLayer, 'segment-common-image')).toBe(
false,
);
expect(layerStartsWithSearch(internalLayer, 'aliyun')).toBe(false);
expect(layerMatchesSearch(visibleLayer, 'audio1.0')).toBe(true);
expect(layerMatchesSearch(visibleLayer, 'vectorengine')).toBe(false);
expect(internalAsset).toMatchObject({
model: 'BgFilter complex',
provider: 'BgFilter',
});
expect(internalLayer).toMatchObject({
model: 'segment-common-image',
provider: 'Aliyun Matting',
});
});
it('renames assets, toggles folders and creates local folders', () => {
expect(renameAssetById([createAsset()], 'asset-a', '新名字')[0]).toMatchObject(
{ label: '新名字' },
@@ -1,3 +1,4 @@
import { isEditorInternalProcessingModel } from './ImageCanvasEditorModel';
import type {
AssetMarqueeState,
CanvasLayer,
@@ -204,13 +205,19 @@ function startsWithSearchText(query: string, values: Array<string | null | undef
return values.some((value) => value?.trim().toLowerCase().startsWith(query));
}
function resolveSearchableEditorModel(model: string | null | undefined) {
const normalizedModel = model?.trim();
return normalizedModel && !isEditorInternalProcessingModel(normalizedModel)
? normalizedModel
: undefined;
}
function buildAssetSearchValues(asset: EditorAsset) {
return [
asset.label,
asset.prompt,
asset.actualPrompt,
asset.model,
asset.provider,
resolveSearchableEditorModel(asset.model),
asset.taskId,
asset.assetKind,
asset.mediaType,
@@ -222,8 +229,7 @@ function buildLayerSearchValues(layer: CanvasLayer) {
layer.title,
layer.prompt,
layer.actualPrompt,
layer.model,
layer.provider,
resolveSearchableEditorModel(layer.model),
layer.taskId,
layer.assetKind,
layer.mediaType,
@@ -282,6 +282,8 @@ describe('ImageCanvasEditorModel', () => {
zIndex: 9,
sourceType: 'generated',
objectKey: 'generated/object.png',
model: 'birefnet',
sourceResourceId: 'resource-provider-source',
assetKind: 'character',
generationInputs: {
fields: [{ title: '角色设定', value: '骑士' }],
@@ -307,6 +309,15 @@ describe('ImageCanvasEditorModel', () => {
fields: [{ title: '角色设定', value: '骑士' }],
references: [],
},
model: 'birefnet',
sourceResourceId: 'resource-provider-source',
},
],
[
'resource-provider-source',
{
imageSrc: '/read/provider-source.png',
model: 'gpt-image-2',
},
],
]),
@@ -318,11 +329,49 @@ describe('ImageCanvasEditorModel', () => {
sourceType: 'generated',
assetKind: 'character',
objectKey: 'generated/object.png',
model: 'gpt-image-2',
locked: true,
});
expect(hydrated?.generationInputs?.fields[0]?.value).toBe('骑士');
});
it('recovers the normal source model after the owner payload removes an internal model', () => {
const hydrated = hydrateLayer(
{
layerId: 'layer-derived',
resourceId: 'resource-derived',
title: '去背景结果',
x: 10,
y: 20,
width: 1024,
height: 768,
originalWidth: 1024,
originalHeight: 768,
zIndex: 9,
sourceType: 'generated',
sourceResourceId: 'resource-source',
},
new Map([
[
'resource-derived',
{
imageSrc: '/read/derived.png',
sourceResourceId: 'resource-source',
},
],
[
'resource-source',
{
imageSrc: '/read/source.png',
model: 'gpt-image-2',
},
],
]),
);
expect(hydrated?.model).toBe('gpt-image-2');
});
it('hydrates object metadata from project resources when the saved layer is lean', () => {
const hydrated = hydrateLayer(
{
@@ -43,6 +43,24 @@ export const MINIMAP_SIZE = { width: 132, height: 84 };
export const MINIMAP_PADDING = 8;
export const MINIMAP_DRAG_SENSITIVITY = 0.3;
export const ASSET_DRAG_MIME_TYPE = 'application/x-genarrative-editor-asset';
const INTERNAL_EDITOR_PROCESSING_MODELS = new Set([
'anime-seg',
'bgfilter complex',
'birefnet',
'connected-components',
'screen-color-keying',
'segment-common-image',
]);
export function isEditorInternalProcessingModel(
model: string | null | undefined,
) {
const normalizedModel = model?.trim().toLowerCase();
return Boolean(
normalizedModel && INTERNAL_EDITOR_PROCESSING_MODELS.has(normalizedModel),
);
}
export const MAX_HISTORY_STEPS = 60;
export const CONTEXT_MENU_VIEWPORT_MARGIN = 8;
export const CONTEXT_MENU_SIZE = {
@@ -734,7 +752,7 @@ export function hydrateLayer(
prompt: stringOrNull(snapshot.prompt) ?? stringOrNull(resource?.prompt),
actualPrompt:
stringOrNull(snapshot.actualPrompt) ?? stringOrNull(resource?.actualPrompt),
model: stringOrNull(snapshot.model) ?? stringOrNull(resource?.model),
model: resolveHydratedLayerModel(snapshot, resource, resourcesById),
provider: stringOrNull(snapshot.provider) ?? stringOrNull(resource?.provider),
taskId: stringOrNull(snapshot.taskId) ?? stringOrNull(resource?.taskId),
objectKey: stringOrNull(snapshot.objectKey) ?? stringOrNull(resource?.objectKey),
@@ -761,6 +779,33 @@ export function hydrateLayer(
};
}
function resolveHydratedLayerModel(
snapshot: EditorProjectLayerSnapshot,
resource: CanvasLayerResourceMetadata | undefined,
resourcesById: Map<string, CanvasLayerResourceMetadata>,
) {
const persistedModel =
stringOrNull(snapshot.model) ?? stringOrNull(resource?.model);
if (persistedModel && !isEditorInternalProcessingModel(persistedModel)) {
return persistedModel;
}
let sourceResourceId =
stringOrNull(snapshot.sourceResourceId) ??
stringOrNull(resource?.sourceResourceId);
const visitedResourceIds = new Set<string>();
while (sourceResourceId && !visitedResourceIds.has(sourceResourceId)) {
visitedResourceIds.add(sourceResourceId);
const sourceResource = resourcesById.get(sourceResourceId);
const sourceModel = stringOrNull(sourceResource?.model);
if (sourceModel && !isEditorInternalProcessingModel(sourceModel)) {
return sourceModel;
}
sourceResourceId = stringOrNull(sourceResource?.sourceResourceId);
}
return persistedModel;
}
export function mapAssetLibrarySnapshot(
library: EditorAssetLibrarySnapshot,
): {
@@ -175,6 +175,7 @@ describe('ImageCanvasExportModel', () => {
it('filters built-in prompts from exported visible generation inputs', () => {
const metadata = buildLayerExportMetadata(
buildLayer({
model: 'gpt-image-2',
generationInputs: {
fields: [
{ title: '生成提示词', value: 'AI 生成图片' },
@@ -195,6 +196,7 @@ describe('ImageCanvasExportModel', () => {
{ title: '用户输入', value: '用户填写的视觉要求' },
{ title: '游戏分类', value: '游戏音效' },
{ title: 'duration', value: '5秒' },
{ title: '处理模型', value: 'birefnet' },
],
references: [],
},
@@ -210,6 +212,7 @@ describe('ImageCanvasExportModel', () => {
],
references: [],
});
expect(metadata.visible.model).toBe('gpt-image-2');
});
it('builds a Spine JSON attachment timeline from image sequence frames', () => {
@@ -8,7 +8,8 @@ import type {
import {
DEFAULT_ICON_DESCRIPTIONS,
formatLayerImageType,
getEditorImageModelDisplayName,
getEditorLayerModelDisplayName,
isEditorUserVisibleGenerationInputField,
UI_DESIGN_ASSET_EXTRACTION_PROMPT,
} from './ImageCanvasGenerationModel';
import { formatCanvasDurationMetric } from './ImageCanvasMediaModel';
@@ -543,6 +544,7 @@ function isBuiltInGenerationInputField(field: CanvasGenerationInputField) {
const builtInValues =
BUILT_IN_GENERATION_INPUT_VALUES_BY_TITLE.get(normalizedTitle);
return (
!isEditorUserVisibleGenerationInputField(field) ||
BUILT_IN_GENERATION_INPUT_TITLES.has(normalizedTitle) ||
normalizedValue === UI_DESIGN_ASSET_EXTRACTION_PROMPT ||
Boolean(builtInValues?.has(normalizedValue))
@@ -572,7 +574,7 @@ export function buildLayerVisibleExportMetadata(layer: CanvasLayer) {
const base = {
type: formatLayerImageType(layer),
generationInputs: buildVisibleGenerationInputs(layer),
model: layer.model ? getEditorImageModelDisplayName(layer.model) : '-',
model: getEditorLayerModelDisplayName(layer.model),
task: formatTaskIdForDisplay(layer.taskId),
object: layer.objectKey ?? layer.assetObjectId ?? '-',
};
@@ -341,7 +341,6 @@ describe('ImageCanvasGenerationLayerModel', () => {
prompt: '图标 prompt',
actualPrompt: '图标 actual prompt',
model: 'gpt-image-2',
provider: 'VectorEngine',
taskId: 'task-icons',
priceMudPoints: 20,
},
@@ -432,7 +431,6 @@ describe('ImageCanvasGenerationLayerModel', () => {
prompt: '图标 prompt',
actualPrompt: '图标 actual prompt',
model: 'gpt-image-2',
provider: 'VectorEngine',
taskId: 'task-icons',
priceMudPoints: 20,
},
@@ -464,7 +462,6 @@ describe('ImageCanvasGenerationLayerModel', () => {
prompt: '金币掉落叮当声',
actualPrompt: '金币掉落叮当声',
model: 'audio1.0',
provider: 'VectorEngine',
taskId: 'sound-task-1',
priceMudPoints: 10,
audioKind: 'sound-effect',
@@ -525,7 +522,6 @@ describe('ImageCanvasGenerationLayerModel', () => {
sourceType: 'generated',
prompt: '金币掉落叮当声',
model: 'audio1.0',
provider: 'VectorEngine',
taskId: 'sound-task-1',
objectKey: 'generated-character-drafts/editor-audios/sfx.mp3',
assetObjectId: 'assetobj-audio-1',
@@ -535,6 +531,7 @@ describe('ImageCanvasGenerationLayerModel', () => {
assetId: 'asset-audio-bff',
}),
});
expect(layer.provider).toBeUndefined();
});
it('creates video result layers with OSS asset identity', () => {
@@ -549,7 +546,6 @@ describe('ImageCanvasGenerationLayerModel', () => {
prompt: '开场动画',
actualPrompt: '开场动画',
model: 'seedance2.0-fast',
provider: 'VectorEngine',
taskId: 'video-task-1',
priceMudPoints: 40,
durationSeconds: 5,
@@ -586,6 +582,7 @@ describe('ImageCanvasGenerationLayerModel', () => {
durationSeconds: 5,
});
expect(layer.generatedAssetSnapshot?.assetId).toBe('asset-video-bff');
expect(layer.provider).toBeUndefined();
});
it('creates character animation result layers as playable image sequences', () => {
@@ -653,11 +650,11 @@ describe('ImageCanvasGenerationLayerModel', () => {
prompt: '待机动作',
actualPrompt: '待机动作',
model: 'seedance2.0-fast',
provider: 'ark',
taskId: 'character-animation-task-1',
assetKind: 'character-animation',
durationSeconds: 4,
});
expect(layer?.generationInputs?.fields[0]?.value).toBe('待机动作');
expect(layer?.provider).toBeUndefined();
});
});
@@ -114,7 +114,6 @@ function applyGeneratedMetadata(
prompt: generated.prompt,
actualPrompt: generated.actualPrompt ?? generated.prompt,
model: generated.model,
provider: generated.provider,
taskId: generated.taskId,
objectKey: resource?.objectKey ?? generated.objectKey,
assetObjectId: resource?.assetObjectId ?? generated.assetObjectId,
@@ -333,7 +332,6 @@ export function createIconSpritesheetResultLayers({
prompt: generated.prompt,
actualPrompt: generated.actualPrompt ?? generated.prompt,
model: generated.model,
provider: generated.provider,
taskId: generated.taskId,
assetKind: 'icon-spritesheet',
sourceAssetId: spritesheetAsset?.assetId,
@@ -382,7 +380,6 @@ export function createIconSpritesheetResultLayers({
prompt: generated.prompt,
actualPrompt: generated.actualPrompt ?? generated.prompt,
model: generated.model,
provider: generated.provider,
taskId: generated.taskId,
assetKind: 'icon',
sourceAssetId: asset?.assetId,
@@ -451,7 +448,6 @@ export function createVideoResultLayer({
prompt: generated.prompt,
actualPrompt: generated.actualPrompt ?? generated.prompt,
model: generated.model,
provider: generated.provider,
taskId: generated.taskId,
objectKey: resource?.objectKey ?? generated.objectKey,
assetObjectId: resource?.assetObjectId ?? generated.assetObjectId,
@@ -513,7 +509,6 @@ export function createAudioResultLayer({
prompt: generated.prompt,
actualPrompt: generated.actualPrompt ?? generated.prompt,
model: generated.model,
provider: generated.provider,
taskId: generated.taskId,
objectKey: resource?.objectKey ?? generated.objectKey,
assetObjectId: resource?.assetObjectId ?? generated.assetObjectId,
@@ -575,7 +570,6 @@ export function createCharacterAnimationResultLayer({
prompt: generated.prompt,
actualPrompt: generated.prompt,
model: generated.model,
provider: 'ark',
taskId: generated.taskId,
assetKind: 'character-animation',
durationSeconds: generated.durationSeconds,
@@ -4,7 +4,10 @@ import type {
EditorCharacterAnimationRatio,
EditorCharacterAnimationResolution,
} from '../../services/image-editor/editorProjectClient';
import { isGeneratedLayer } from './ImageCanvasEditorModel';
import {
isEditorInternalProcessingModel,
isGeneratedLayer,
} from './ImageCanvasEditorModel';
import type {
CanvasGenerationDialogState,
CanvasGenerationInputField,
@@ -548,6 +551,21 @@ export function getEditorImageModelDisplayName(
);
}
export function isEditorUserVisibleGenerationInputField(
field: CanvasGenerationInputField,
) {
return field.title.trim() !== '处理模型';
}
export function getEditorLayerModelDisplayName(
model: string | null | undefined,
) {
if (!model?.trim() || isEditorInternalProcessingModel(model)) {
return '-';
}
return getEditorImageModelDisplayName(model);
}
export function getEditorGenerationModelDisplayName(
model: string | null | undefined,
) {
@@ -58,6 +58,7 @@ describe('ImageCanvasMetadataModalView', () => {
expect(within(dialog).getByText('参考图')).toBeTruthy();
expect(within(dialog).getByText('角色立绘')).toBeTruthy();
expect(within(dialog).getByText('项目资源 · resource-reference')).toBeTruthy();
expect(within(dialog).getByText('Model')).toBeTruthy();
expect(within(dialog).getByText('gpt-image-2')).toBeTruthy();
expect(within(dialog).getByText('1024 x 768 px')).toBeTruthy();
expect(within(dialog).queryByText('Provider')).toBeNull();
@@ -66,6 +67,54 @@ describe('ImageCanvasMetadataModalView', () => {
expect(within(dialog).getByText('123')).toBeTruthy();
});
it('keeps the generation model while hiding internal processing models', () => {
const generationInputsWithInternalMattingMetadata = {
fields: [
{ title: '处理阶段', value: '角色抠图' },
{ title: '处理模型', value: 'birefnet' },
],
references: [],
mattingProvider: 'Aliyun Matting',
mattingModel: 'segment-common-image',
};
const { rerender } = render(
<ImageCanvasMetadataModalView
layer={createLayer({
model: 'gpt-image-2',
generationInputs: generationInputsWithInternalMattingMetadata,
})}
onClose={vi.fn()}
/>,
);
const dialog = screen.getByRole('dialog', { name: '图片信息' });
expect(within(dialog).getByText('处理阶段')).toBeTruthy();
expect(within(dialog).getByText('角色抠图')).toBeTruthy();
expect(within(dialog).queryByText('处理模型')).toBeNull();
expect(within(dialog).getByText('Model')).toBeTruthy();
expect(within(dialog).getByText('gpt-image-2')).toBeTruthy();
expect(within(dialog).queryByText('birefnet')).toBeNull();
expect(within(dialog).queryByText('Aliyun Matting')).toBeNull();
expect(within(dialog).queryByText('segment-common-image')).toBeNull();
rerender(
<ImageCanvasMetadataModalView
layer={createLayer({
model: 'birefnet',
generationInputs: {
fields: [{ title: '处理阶段', value: '角色抠图' }],
references: [],
},
})}
onClose={vi.fn()}
/>,
);
expect(within(dialog).getByText('Model')).toBeTruthy();
expect(within(dialog).queryByText('birefnet')).toBeNull();
});
it('renders nanobanana2 instead of the raw Gemini image model id', () => {
render(
<ImageCanvasMetadataModalView
@@ -3,7 +3,8 @@ import type { CanvasLayer } from './ImageCanvasEditorTypes';
import { formatTaskIdForDisplay } from './ImageCanvasExportModel';
import {
formatLayerImageType,
getEditorImageModelDisplayName,
getEditorLayerModelDisplayName,
isEditorUserVisibleGenerationInputField,
} from './ImageCanvasGenerationModel';
import { formatCanvasDurationMetric } from './ImageCanvasMediaModel';
@@ -27,6 +28,11 @@ export function ImageCanvasMetadataModalView({
layer,
onClose,
}: ImageCanvasMetadataModalViewProps) {
const visibleGenerationInputFields =
layer?.generationInputs?.fields.filter(
isEditorUserVisibleGenerationInputField,
) ?? [];
const generationInputReferences = layer?.generationInputs?.references ?? [];
const mediaLabel =
layer?.mediaType === 'audio'
? '音频'
@@ -52,10 +58,10 @@ export function ImageCanvasMetadataModalView({
<dd>{formatLayerImageType(layer)}</dd>
<dt>生成输入</dt>
<dd className="image-canvas-editor__metadata-inputs">
{layer.generationInputs?.fields.length ||
layer.generationInputs?.references.length ? (
{visibleGenerationInputFields.length ||
generationInputReferences.length ? (
<>
{layer.generationInputs.fields.map((field) => (
{visibleGenerationInputFields.map((field) => (
<div
key={`${field.title}-${field.value}`}
className="image-canvas-editor__metadata-input-field"
@@ -66,9 +72,9 @@ export function ImageCanvasMetadataModalView({
<span>{field.value}</span>
</div>
))}
{layer.generationInputs.references.length ? (
{generationInputReferences.length ? (
<div className="image-canvas-editor__metadata-reference-list">
{layer.generationInputs.references.map((reference) => (
{generationInputReferences.map((reference) => (
<div
key={`${reference.title}-${reference.label}-${reference.refType}-${reference.refId}`}
className="image-canvas-editor__metadata-reference-card"
@@ -93,7 +99,7 @@ export function ImageCanvasMetadataModalView({
)}
</dd>
<dt>Model</dt>
<dd>{layer.model ? getEditorImageModelDisplayName(layer.model) : '-'}</dd>
<dd>{getEditorLayerModelDisplayName(layer.model)}</dd>
{layer.mediaType === 'audio' ? (
<>
<dt>时长</dt>
@@ -269,6 +269,52 @@ describe('ImageCanvasSidebarView', () => {
).toBeNull();
});
it('does not expose internal models or providers through asset search', () => {
const folder = createFolder();
const internalAsset = createAsset({
id: 'asset-internal',
label: '透明角色',
sourceType: 'generated',
model: 'BgFilter complex',
provider: 'BgFilter',
});
const visibleAsset = createAsset({
id: 'asset-visible',
label: '生成角色',
sourceType: 'generated',
model: 'gpt-image-2',
provider: 'VectorEngine',
});
render(
<ImageCanvasSidebarView
{...createSidebarProps({
assets: [internalAsset, visibleAsset],
groupedAssets: [
{ ...folder, assets: [internalAsset, visibleAsset] },
],
})}
/>,
);
const sidebar = screen.getByRole('complementary', { name: '图片资源栏' });
const search = within(sidebar).getByLabelText('搜索素材');
fireEvent.change(search, { target: { value: 'BgFilter' } });
expect(
within(sidebar).queryByRole('button', { name: '添加透明角色' }),
).toBeNull();
fireEvent.change(search, { target: { value: 'VectorEngine' } });
expect(
within(sidebar).queryByRole('button', { name: '添加生成角色' }),
).toBeNull();
fireEvent.change(search, { target: { value: 'gpt-image-2' } });
expect(
within(sidebar).getByRole('button', { name: '添加生成角色' }),
).toBeTruthy();
});
it('renders the layer panel with grouped and locked layer details', () => {
render(
<ImageCanvasSidebarView
@@ -346,6 +392,56 @@ describe('ImageCanvasSidebarView', () => {
).toBeNull();
});
it('does not expose internal models or providers through layer search', () => {
render(
<ImageCanvasSidebarView
{...createSidebarProps({
activeSidebarPanel: 'layers',
layers: [
createLayer({
id: 'layer-internal',
title: '透明角色图层',
model: 'segment-common-image',
provider: 'Aliyun Matting',
zIndex: 2,
}),
createLayer({
id: 'layer-visible',
title: '生成角色图层',
model: 'gpt-image-2',
provider: 'VectorEngine',
zIndex: 1,
}),
],
})}
/>,
);
const sidebar = screen.getByRole('complementary', { name: '图片资源栏' });
const search = within(sidebar).getByLabelText('搜索画布图层');
fireEvent.change(search, { target: { value: 'segment-common-image' } });
expect(
within(sidebar).queryByRole('button', {
name: '选择图层透明角色图层',
}),
).toBeNull();
fireEvent.change(search, { target: { value: 'VectorEngine' } });
expect(
within(sidebar).queryByRole('button', {
name: '选择图层生成角色图层',
}),
).toBeNull();
fireEvent.change(search, { target: { value: 'gpt-image-2' } });
expect(
within(sidebar).getByRole('button', {
name: '选择图层生成角色图层',
}),
).toBeTruthy();
});
it('keeps sidebar tab switching and repeated-click closing behavior', () => {
render(<SidebarTabsHarness />);
@@ -304,7 +304,6 @@ export type EditorImageGenerationResult = {
prompt: string;
actualPrompt?: string | null;
model: string;
provider: string;
taskId: string;
resource?: EditorProjectResourceSnapshot | null;
asset?: EditorAssetSnapshot | null;
@@ -342,7 +341,6 @@ export type EditorIconSpritesheetGenerationResult = {
prompt: string;
actualPrompt?: string | null;
model: string;
provider: string;
taskId: string;
priceMudPoints: number;
spritesheetResource?: EditorProjectResourceSnapshot | null;
@@ -465,7 +463,6 @@ export type EditorVideoGenerationResult = {
prompt: string;
actualPrompt?: string | null;
model: string;
provider: string;
taskId: string;
durationSeconds: number;
priceMudPoints?: number;
@@ -506,7 +503,6 @@ export type EditorAudioGenerationResult = {
prompt: string;
actualPrompt?: string | null;
model: string;
provider: string;
taskId: string;
priceMudPoints: number;
audioKind: 'sound-effect' | 'background-music';