diff --git a/docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md b/docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md
index aeffa21ca..fca149a6e 100644
--- a/docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md
+++ b/docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md
@@ -85,9 +85,9 @@
## 生成规范参考图
-- `生成规范 -> 角色规范`、`图标规范`、`图标规范`、`自定义规范` 的设定面板都支持上传 1 张参考图,并统一放在面板首行参考图区域。
+- `生成规范 -> 角色规范`、`自定义规范` 的设定面板支持上传多张参考图,最多 5 张;`图标规范` 保持单张规范参考图。所有参考图统一放在面板首行参考图区域,并可逐张删除。
- 参考图入口只展示字段标题、缩略图或上传图标、文件名,不把参考规则说明铺在 UI 上。
-- 提交生成规范时,若存在参考图,前端必须把参考图作为 `referenceImageSrcs[0]` 提交到 `/api/editor/images/generations`,并在生图提示词开头自动追加“参考图生成规范”语义:要求模型参考图 1 的构图、风格、材质、色彩、形状语言和视觉层级生成规范图,但不要复制参考图中的文字、水印或无关背景。
+- 提交生成规范时,若存在参考图,前端必须按用户当前顺序把全部参考图提交到 `/api/editor/images/generations` 的 `referenceImageSrcs`,并在生图提示词开头自动追加“参考图生成规范”语义:要求模型参考图的构图、风格、材质、色彩、形状语言和视觉层级生成规范图,但不要复制参考图中的文字、水印或无关背景。
- 生成结果的信息快照必须记录该参考图,标题为 `参考图`,便于后续在图片信息面板回看生成输入。
## 新建生成图落点避让
diff --git a/src/components/image-editor/ImageCanvasGenerationModel.test.ts b/src/components/image-editor/ImageCanvasGenerationModel.test.ts
index d0fd5bb6a..cc74bf77f 100644
--- a/src/components/image-editor/ImageCanvasGenerationModel.test.ts
+++ b/src/components/image-editor/ImageCanvasGenerationModel.test.ts
@@ -52,6 +52,7 @@ import {
isNormalizedCanvasGenerationInputs,
isQuickEditSupportedLayer,
resolveCharacterAnimationSourceImageSrc,
+ resolveDialogExtraImageReferenceLimit,
resolveEditorImageGenerationPixelSize,
resolveImageGenerationErrorMessage,
VIDEO_MODEL_KLING_3,
@@ -68,6 +69,26 @@ describe('ImageCanvasGenerationModel', () => {
});
});
+ it('uses the fixed spec model provider limit for multi-reference spec dialogs', () => {
+ expect(
+ resolveDialogExtraImageReferenceLimit({
+ mode: 'spec',
+ specType: 'character',
+ prompt: '',
+ status: 'idle',
+ }),
+ ).toBe(5);
+ expect(
+ resolveDialogExtraImageReferenceLimit({
+ mode: 'spec',
+ specType: 'custom',
+ prompt: '',
+ status: 'idle',
+ imageModel: IMAGE_MODEL_NANOBANANA2,
+ }),
+ ).toBe(5);
+ });
+
it('展示时长时收敛到两位小数且不改动其他字段', () => {
const format = (title: string, value: string) =>
formatEditorGenerationInputFieldValue({ title, value });
diff --git a/src/components/image-editor/ImageCanvasGenerationModel.ts b/src/components/image-editor/ImageCanvasGenerationModel.ts
index 724ff72b1..bc63f4ddc 100644
--- a/src/components/image-editor/ImageCanvasGenerationModel.ts
+++ b/src/components/image-editor/ImageCanvasGenerationModel.ts
@@ -464,7 +464,8 @@ export function resolveExtraImageReferenceLimit(
export function resolveDialogExtraImageReferenceLimit(
dialog: GenerateDialogState,
- model: string | null | undefined = dialog.imageModel,
+ model: string | null | undefined = dialog.imageModel ??
+ (dialog.mode === 'spec' ? SPEC_GENERATION_MODEL : undefined),
) {
if (dialog.mode === 'icon') {
return resolveExtraImageReferenceLimit(
@@ -494,7 +495,10 @@ export function resolveDialogExtraImageReferenceLimit(
1,
);
}
- return IMAGE_GENERATION_REFERENCE_LIMIT;
+ return resolveExtraImageReferenceLimit(
+ model,
+ IMAGE_GENERATION_REFERENCE_LIMIT,
+ );
}
export function countDialogExtraImageReferences(dialog: GenerateDialogState) {
diff --git a/src/components/image-editor/ImageCanvasReferenceSlot.test.tsx b/src/components/image-editor/ImageCanvasReferenceSlot.test.tsx
index b4c730ac9..0a67e9b54 100644
--- a/src/components/image-editor/ImageCanvasReferenceSlot.test.tsx
+++ b/src/components/image-editor/ImageCanvasReferenceSlot.test.tsx
@@ -1,6 +1,6 @@
/* @vitest-environment jsdom */
-import { render } from '@testing-library/react';
+import { render, screen } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ImageCanvasReferenceSlot } from './ImageCanvasReferenceSlot';
@@ -14,7 +14,10 @@ vi.mock('../../hooks/useResolvedAssetReadUrl', () => ({
describe('ImageCanvasReferenceSlot', () => {
beforeEach(() => {
useResolvedAssetReadUrlMock.mockImplementation(
- (source: string | null | undefined, options?: { objectKey?: string | null }) => ({
+ (
+ source: string | null | undefined,
+ options?: { objectKey?: string | null },
+ ) => ({
resolvedUrl: options?.objectKey
? `https://oss.example.com/${options.objectKey}?signature=1`
: (source ?? ''),
@@ -49,4 +52,21 @@ describe('ImageCanvasReferenceSlot', () => {
'https://oss.example.com/generated-character-drafts/editor/legacy-resources/editor-resource-1.png?signature=1',
);
});
+
+ it('keeps an explicitly marked reference remove action visible', () => {
+ render(
+ }
+ imageSrc="/reference.png"
+ label="参考图1"
+ ariaLabel="参考图1"
+ onRemove={vi.fn()}
+ alwaysShowRemove
+ />,
+ );
+
+ expect(
+ screen.getByRole('button', { name: '删除参考图1' }).className,
+ ).toContain('image-canvas-editor__reference-chip-remove--always-visible');
+ });
});
diff --git a/src/components/image-editor/ImageCanvasReferenceSlot.tsx b/src/components/image-editor/ImageCanvasReferenceSlot.tsx
index f0304f6e2..8a515570f 100644
--- a/src/components/image-editor/ImageCanvasReferenceSlot.tsx
+++ b/src/components/image-editor/ImageCanvasReferenceSlot.tsx
@@ -28,6 +28,7 @@ type ImageCanvasReferenceSlotProps = {
onClick?: () => void;
onRemove?: () => void;
removeLabel?: string;
+ alwaysShowRemove?: boolean;
};
export function ImageCanvasReferenceSlot({
@@ -45,6 +46,7 @@ export function ImageCanvasReferenceSlot({
onClick,
onRemove,
removeLabel,
+ alwaysShowRemove = false,
}: ImageCanvasReferenceSlotProps) {
const hasPreviewImage = Boolean(imageSrc?.trim() || objectKey?.trim());
const slotClassName = [
@@ -106,7 +108,11 @@ export function ImageCanvasReferenceSlot({
{onRemove ? (