修复图标规范上传类型合同
图标规范上传在项目资源与账号素材分支统一登记为 icon-spec。 补充上传后提交图集生成及两条持久化分支回归测试。 同步编辑器设计文档、外部 API Skill 与 Python helper 的 referenceId 合同。
This commit is contained in:
@@ -17,7 +17,7 @@ Prefer `scripts/genarrative_external_api.py` for runnable REST calls. It uses on
|
||||
2. Before the first generation in a new conversation, obtain a canvas name unless an existing `projectId` and `assetFolderId` were supplied. Create or reuse a project and a same-name asset-library folder. Retain `canvasName`, `projectId`, `assetFolderId`, and the current art spec.
|
||||
3. Normalize art requests into a reusable spec. Ask only for missing values that block the selected operation. Reuse the spec until the user changes its style, subject family, palette, format, or constraints.
|
||||
4. Infer the operation from the user's intent. Do not ask the user to select an API unless two operations would produce materially different artifacts.
|
||||
5. If a reference exists only as a local file, upload and confirm it first. Pass the stable returned `objectKey` to generation; never substitute a temporary signed URL.
|
||||
5. If a reference exists only as a local file, upload and confirm it first. Pass the stable returned `objectKey` to operations that accept object references; never substitute a temporary signed URL. For an icon-spritesheet primary spec, additionally create a project resource or asset record with `assetKind="icon-spec"`, then pass the returned resource or asset ID as `referenceId`.
|
||||
6. For generation endpoints that support the fields, include `projectId`, `assetFolderId`, an asset label, and `canvasCompletion` so the result enters both the canvas and its same-name library folder.
|
||||
7. Treat every generation POST as asynchronous. Send one stable `Idempotency-Key` per logical request, retain the returned `operationId`, and poll the returned `statusUrl` or `GET /api/external/v1/generations/{operationId}` according to `pollAfterMs`.
|
||||
8. Consume `result` only after `status=completed`. On `failed`, surface the safe error. On a client timeout or lost response, retain the operation/key; do not create a replacement request.
|
||||
|
||||
@@ -44,7 +44,7 @@ Every generation row requires a stable `Idempotency-Key` header and returns HTTP
|
||||
| --- | --- | --- | --- |
|
||||
| Image generation | `/api/external/v1/editor/images/generations` | `prompt` | `kind`, `style`, `model`, `aspectRatio`, `imageSize`, `size`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
|
||||
| Image edit/redraw | `/api/external/v1/editor/images/edits` | `prompt`, `sourceImageSrc` | `referenceImageSrcs`, `model`, `size`, `projectId`, `assetFolderId`, `assetLabel`, `sourceResourceId`, `targetLayerId`, `canvasCompletion` |
|
||||
| Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceImageSrc`, `iconDescriptions` | `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
| Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceId`, `iconDescriptions` | `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
| UI asset extraction | `/api/external/v1/editor/ui-designs/assets/extractions` | `sourceImageSrc`, `aspectRatio`, `imageSize` | `screenColor`, `model`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `spritesheetLabel`, `canvasCompletion` |
|
||||
| Character animation | `/api/external/v1/editor/character-animations/generations` | `sourceLayerId`, `sourceImageSrc`, `sourceWidth`, `sourceHeight`, `promptText`, `resolution`, `ratio`, `frameCount`, `durationSeconds`, `model` | `projectId`, `sourceResourceId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
| Video generation | `/api/external/v1/editor/videos/generations` | `prompt`, `model`, `aspectRatio`, `durationSeconds`, `resolution`, `mode`, `sound` | `referenceImageSrcs`, `referenceVideoSrcs`, `referenceAudioSrcs`, `webSearchEnabled`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
@@ -70,19 +70,21 @@ Supply the `operationId` returned by submission. Poll no faster than `pollAfterM
|
||||
|
||||
## Reference Field Mapping
|
||||
|
||||
After confirming a local upload, pass its stable `objectKey` into:
|
||||
After confirming a local upload, pass its stable `objectKey` into operations that accept object references:
|
||||
|
||||
| Target capability | Field |
|
||||
| --- | --- |
|
||||
| Image generation | `referenceImageSrcs` |
|
||||
| Image edit/redraw | `sourceImageSrc`; additional references in `referenceImageSrcs` |
|
||||
| Icon spritesheet | `referenceImageSrc`; additional style references in `referenceImageSrcs` |
|
||||
| Icon spritesheet | Register the primary spec as an `assetKind="icon-spec"` project resource or asset, then pass its returned ID as `referenceId`; additional style references remain in `referenceImageSrcs` |
|
||||
| UI design extraction | `sourceImageSrc`; additional references in `referenceImageSrcs` |
|
||||
| Character animation | `sourceImageSrc` |
|
||||
| Video with image references | `referenceImageSrcs` |
|
||||
|
||||
Use video/audio reference arrays only with models that support them. Do not pass an expiring signed read URL as a generation reference.
|
||||
|
||||
The icon-spritesheet primary `referenceId` is intentionally stricter than ordinary image references: it accepts only a current-owner project resource ID or asset ID whose authoritative `assetKind` is `icon-spec`. It does not accept an `objectKey`, URL, Data URL, or Blob URL.
|
||||
|
||||
## Common Values
|
||||
|
||||
Use OpenAPI as the final authority; these common values are a routing aid:
|
||||
|
||||
@@ -78,7 +78,7 @@ Keep the existing autonomous-build task graph. Do not add a parallel task system
|
||||
|
||||
1. `art-director` generates `assets/art-spec.png` with image generation, `kind: "spec"`, then registers it as `assetKind: "icon-spec"`. This image is the authoritative visual spec; `generationInputs.artSpec` is supporting structured context.
|
||||
2. `design-foundation` generates `assets/ui-prototype.png` with `kind: "ui-design"`, using the registered art-spec resource ID in `referenceImageSrcs`.
|
||||
3. `art-asset-plan` generates transparent `assets/art-spritesheet.png` through icon spritesheet generation, using the same registered art-spec resource ID as `referenceImageSrc` plus concrete `iconDescriptions`.
|
||||
3. `art-asset-plan` generates transparent `assets/art-spritesheet.png` through icon spritesheet generation, using the same registered art-spec resource ID as `referenceId` plus concrete `iconDescriptions`.
|
||||
|
||||
For a playable Canvas game, do not stop at generation. Make `code-prototype` depend on `art-asset-plan` and consume the persisted `iconImageSrcs` slices for core players, blocks or targets, scene obstacles, and feedback. For the four-category game-chat contract, require exactly four slices before registering the local runtime sheet; both fewer and extra components fail closed. Treat `art-spec.png` as reference-only. A full-sheet `<img>`, CSS background, path-only mention, guessed equal-grid crop, or code-drawn replacement for core entities is not runtime asset use. If slicing produces `sliceWarning`, keep the complete transparent sheet as a valid editor artifact, but fail the playable game asset gate until real slice files or verified atlas coordinates exist; never invent coordinates or replace the icon-spritesheet route with ordinary image generation.
|
||||
|
||||
|
||||
@@ -167,6 +167,8 @@ client.generate_image(
|
||||
)
|
||||
```
|
||||
|
||||
Icon spritesheet generation has a stricter primary-spec contract. After upload confirmation, create a project resource or asset record with `assetKind: "icon-spec"`, retain its returned `resourceId` or `assetId`, and pass that ID as `referenceId`. The primary spec does not accept the uploaded `objectKey` directly; only additional style references may continue to use stable object keys in `referenceImageSrcs`.
|
||||
|
||||
For character animation from a local-only source, use actual dimensions and a stable synthetic layer ID:
|
||||
|
||||
```json
|
||||
|
||||
@@ -560,10 +560,13 @@ class GenarrativeExternalClient:
|
||||
|
||||
def generate_icon_spritesheet(
|
||||
self,
|
||||
reference_image_src: str,
|
||||
reference_id: str,
|
||||
icon_descriptions: list[str],
|
||||
**fields: Any,
|
||||
) -> Any:
|
||||
reference_id = normalize_optional_text(reference_id)
|
||||
if not reference_id:
|
||||
raise GenarrativeApiError("reference_id must be a registered icon-spec resource or asset ID")
|
||||
descriptions = [item.strip() for item in icon_descriptions if item.strip()]
|
||||
if not descriptions:
|
||||
raise GenarrativeApiError("icon_descriptions must contain at least one non-empty item")
|
||||
@@ -574,7 +577,7 @@ class GenarrativeExternalClient:
|
||||
return self.submit_and_wait_generation(
|
||||
"/api/external/v1/editor/icon-spritesheets/generations",
|
||||
{
|
||||
"referenceImageSrc": reference_image_src,
|
||||
"referenceId": reference_id,
|
||||
"iconDescriptions": descriptions,
|
||||
**fields,
|
||||
},
|
||||
@@ -768,7 +771,7 @@ def _self_test() -> None:
|
||||
assetLabel="贪吃蛇透明图集",
|
||||
)
|
||||
assert calls[0]["path"] == "/api/external/v1/editor/icon-spritesheets/generations"
|
||||
assert calls[0]["body"]["referenceImageSrc"] == "editor-resource-spec"
|
||||
assert calls[0]["body"]["referenceId"] == "editor-resource-spec"
|
||||
assert calls[0]["body"]["screenColor"] == "auto"
|
||||
assert calls[0]["body"]["iconDescriptions"][0] == "蛇头向上"
|
||||
assert calls[1]["path"] == "/api/external/v1/generations/task-operation-demo"
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
- 点击后弹出菜单:`从画布中选择`、`新建图标规范`、`上传图片`。
|
||||
- `从画布中选择` 进入画布点选状态,只允许选择 `assetKind: "icon-spec"` 的图标规范图片;其它图片点击无效。
|
||||
- `新建图标规范` 复用生成规范表单,规格类型为 `图标规范`,生成成功后图层标记为 `icon-spec`。
|
||||
- `上传图片` 使用现有本地图片上传入口,上传图只绑定到本次面板,不自动放入画布。
|
||||
- `上传图片` 使用现有本地图片上传入口,上传图只绑定到本次面板,不自动放入画布;存在当前项目时创建 `assetKind: "icon-spec"` 的项目资源,不存在当前项目时创建同类型账号素材,提交图集生成时只使用持久化返回的 `resourceId` 或 `assetId`,不得按通用 `image` 类型登记。
|
||||
2. 第二模块为素材描述文本框。
|
||||
- UI 复用角色形象生成面板同款单个文本输入框,让用户直接叙述多个素材。
|
||||
- 默认按换行填入:`返回按钮`、`设置按钮`、`下一关按钮`、`提示按钮`、`原图按钮`、`冻结按钮`。
|
||||
|
||||
@@ -3187,6 +3187,90 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
expect(screen.queryByAltText('画布图片:背包按钮')).toBeNull();
|
||||
});
|
||||
|
||||
it('uploads an authoritative icon spec before submitting icon spritesheet generation', async () => {
|
||||
uploadEditorMediaAssetFileMock.mockResolvedValueOnce({
|
||||
objectKey: 'generated-character-drafts/editor/icon-specs/uploaded.png',
|
||||
assetObjectId: 'asset-object-uploaded-icon-spec',
|
||||
legacyPublicPath:
|
||||
'/generated-character-drafts/editor/icon-specs/uploaded.png',
|
||||
src: 'https://oss.example.com/icon-specs/uploaded.png',
|
||||
});
|
||||
createEditorProjectResourceMock.mockImplementationOnce(
|
||||
async (projectId, input) => ({
|
||||
resourceId: 'resource-uploaded-icon-spec',
|
||||
projectId,
|
||||
imageSrc: input.imageSrc,
|
||||
objectKey: input.objectKey,
|
||||
assetObjectId: input.assetObjectId,
|
||||
width: input.width,
|
||||
height: input.height,
|
||||
sourceType: input.sourceType,
|
||||
assetKind: input.assetKind,
|
||||
generationInputs: input.generationInputs,
|
||||
}),
|
||||
);
|
||||
generateEditorIconSpritesheetMock.mockResolvedValueOnce({
|
||||
spritesheetImageSrc: 'data:image/png;base64,dXBsb2FkZWQtc2hlZXQ=',
|
||||
spritesheetWidth: 512,
|
||||
spritesheetHeight: 512,
|
||||
iconImageSrcs: [],
|
||||
prompt: '图标 prompt',
|
||||
actualPrompt: '图标 prompt',
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
provider: 'VectorEngine',
|
||||
taskId: 'icon-task-uploaded-spec-1',
|
||||
});
|
||||
render(<ImageCanvasEditorView />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成图标素材' }));
|
||||
const iconPanel = screen.getByRole('dialog', { name: '生成图标素材' });
|
||||
fireEvent.click(
|
||||
within(iconPanel).getByRole('button', { name: '图标规范' }),
|
||||
);
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '上传图片' }));
|
||||
await userEvent.upload(
|
||||
screen.getByLabelText('上传媒体文件'),
|
||||
new File(['uploaded-icon-spec'], '上传图标规范.png', {
|
||||
type: 'image/png',
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
within(iconPanel).getByRole('button', { name: '上传图标规范.png' }),
|
||||
).toBeTruthy();
|
||||
});
|
||||
expect(createEditorProjectResourceMock).toHaveBeenCalledWith(
|
||||
'editor-project-default',
|
||||
expect.objectContaining({
|
||||
objectKey: 'generated-character-drafts/editor/icon-specs/uploaded.png',
|
||||
assetKind: 'icon-spec',
|
||||
sourceType: 'uploaded',
|
||||
}),
|
||||
);
|
||||
|
||||
fireEvent.change(
|
||||
within(iconPanel).getByRole('textbox', { name: '素材描述' }),
|
||||
{ target: { value: '上传后的返回按钮' } },
|
||||
);
|
||||
fireEvent.click(within(iconPanel).getByRole('button', { name: '生成' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(generateEditorIconSpritesheetMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
referenceId: 'resource-uploaded-icon-spec',
|
||||
iconDescriptions: ['上传后的返回按钮'],
|
||||
projectId: 'editor-project-default',
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(
|
||||
createEditorProjectResourceMock.mock.invocationCallOrder[0],
|
||||
).toBeLessThan(
|
||||
generateEditorIconSpritesheetMock.mock.invocationCallOrder[0]!,
|
||||
);
|
||||
});
|
||||
|
||||
it('exits character generation canvas picking with Escape', () => {
|
||||
render(<ImageCanvasEditorView />);
|
||||
|
||||
|
||||
@@ -333,6 +333,18 @@ function UploadWorkflowHarness({
|
||||
>
|
||||
准备规范生成
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setGenerateDialog({
|
||||
mode: 'icon',
|
||||
prompt: '',
|
||||
status: 'idle',
|
||||
})
|
||||
}
|
||||
>
|
||||
准备图标素材生成
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
@@ -372,6 +384,12 @@ function UploadWorkflowHarness({
|
||||
>
|
||||
选择角色规范
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => workflow.setUploadTarget('icon-spec')}
|
||||
>
|
||||
选择图标规范
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => workflow.requestUpload('character-spec')}
|
||||
@@ -753,6 +771,7 @@ describe('useImageCanvasUploadWorkflow', () => {
|
||||
width: 420,
|
||||
height: 315,
|
||||
sourceType: 'uploaded',
|
||||
assetKind: 'image',
|
||||
}),
|
||||
);
|
||||
expect(createEditorAssetMock).not.toHaveBeenCalled();
|
||||
@@ -1146,6 +1165,57 @@ describe('useImageCanvasUploadWorkflow', () => {
|
||||
expect(createEditorAssetMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('persists uploaded icon specs as icon-spec project resources', async () => {
|
||||
render(<UploadWorkflowHarness />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '准备图标素材生成' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '选择图标规范' }));
|
||||
fireEvent.change(screen.getByLabelText('上传图片文件'), {
|
||||
target: {
|
||||
files: [createTestFile('上传图标规范.png')],
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('dialog').textContent).toContain(
|
||||
'icon:idle:-:0:上传图标规范.png',
|
||||
);
|
||||
});
|
||||
expect(createEditorProjectResourceMock).toHaveBeenCalledWith(
|
||||
'project-1',
|
||||
expect.objectContaining({
|
||||
assetKind: 'icon-spec',
|
||||
sourceType: 'uploaded',
|
||||
}),
|
||||
);
|
||||
expect(createEditorAssetMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('persists uploaded icon specs as icon-spec assets without a project', async () => {
|
||||
render(<UploadWorkflowHarness projectId={null} />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '准备图标素材生成' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '选择图标规范' }));
|
||||
fireEvent.change(screen.getByLabelText('上传图片文件'), {
|
||||
target: {
|
||||
files: [createTestFile('素材库图标规范.png')],
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('dialog').textContent).toContain(
|
||||
'icon:idle:-:0:素材库图标规范.png',
|
||||
);
|
||||
});
|
||||
expect(createEditorAssetMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
assetKind: 'icon-spec',
|
||||
sourceType: 'uploaded',
|
||||
}),
|
||||
);
|
||||
expect(createEditorProjectResourceMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reserves the single spec reference slot across overlapping uploads', async () => {
|
||||
const alertMock = vi.spyOn(window, 'alert').mockImplementation(() => {});
|
||||
const uploadGate = createDeferred<void>();
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
resolveUploadAssetFolder,
|
||||
} from './ImageCanvasAssetLibraryModel';
|
||||
import type {
|
||||
CanvasAssetKind,
|
||||
CanvasHistoryAction,
|
||||
CanvasLayer,
|
||||
CanvasMediaType,
|
||||
@@ -293,6 +294,7 @@ export function useImageCanvasUploadWorkflow({
|
||||
objectKey,
|
||||
assetObjectId,
|
||||
reservation,
|
||||
authoritativeAssetKind,
|
||||
}: {
|
||||
idPrefix: string;
|
||||
index?: number;
|
||||
@@ -306,6 +308,7 @@ export function useImageCanvasUploadWorkflow({
|
||||
objectKey?: string | null;
|
||||
assetObjectId?: string | null;
|
||||
reservation?: ImageReferenceUploadReservation;
|
||||
authoritativeAssetKind?: CanvasAssetKind;
|
||||
}): Promise<CharacterReferenceImage> => {
|
||||
const resolvedMediaType = mediaType ?? 'image';
|
||||
let persistedImageSrc = imageSrc;
|
||||
@@ -330,7 +333,8 @@ export function useImageCanvasUploadWorkflow({
|
||||
) {
|
||||
throw new ImageReferenceCapacityChangedError();
|
||||
}
|
||||
const assetKind = resolveUploadedAssetKind(resolvedMediaType);
|
||||
const assetKind =
|
||||
authoritativeAssetKind ?? resolveUploadedAssetKind(resolvedMediaType);
|
||||
const generationInputs = buildUploadedMediaGenerationInputs(
|
||||
resolvedMediaType,
|
||||
durationSeconds,
|
||||
@@ -419,6 +423,7 @@ export function useImageCanvasUploadWorkflow({
|
||||
durationSeconds?: number;
|
||||
objectKey?: string | null;
|
||||
assetObjectId?: string | null;
|
||||
authoritativeAssetKind?: CanvasAssetKind;
|
||||
}>,
|
||||
reservation?: ImageReferenceUploadReservation,
|
||||
) => {
|
||||
@@ -1174,6 +1179,7 @@ export function useImageCanvasUploadWorkflow({
|
||||
file: imageFile,
|
||||
fallbackLabel: '图标规范',
|
||||
imageSrc,
|
||||
authoritativeAssetKind: 'icon-spec',
|
||||
},
|
||||
],
|
||||
reservation,
|
||||
|
||||
Reference in New Issue
Block a user