Merge branch 'codex/editor-asset-library' of https://git.genarrative.world/git/GenarrativeAI/Genarrative into codex/editor-asset-library
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { requestJson } from '../apiClient';
|
||||
import { getSignedAssetReadUrl } from '../assetReadUrlService';
|
||||
import { uploadEditorMediaAssetFile } from './editorMediaAssetUploadClient';
|
||||
|
||||
vi.mock('../apiClient', () => ({
|
||||
requestJson: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../assetReadUrlService', () => ({
|
||||
getSignedAssetReadUrl: vi.fn(),
|
||||
}));
|
||||
|
||||
const requestJsonMock = vi.mocked(requestJson);
|
||||
const getSignedAssetReadUrlMock = vi.mocked(getSignedAssetReadUrl);
|
||||
|
||||
describe('editorMediaAssetUploadClient', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.spyOn(Date, 'now').mockReturnValue(1771400000000);
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(async () => new Response(null, { status: 200 })),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('uploads editor MP4 assets through direct OSS upload and confirms object metadata', async () => {
|
||||
requestJsonMock
|
||||
.mockResolvedValueOnce({
|
||||
upload: {
|
||||
bucket: 'bucket',
|
||||
host: 'https://oss.example.com',
|
||||
objectKey:
|
||||
'generated-character-drafts/editor/asset-library/video/demo.mp4',
|
||||
legacyPublicPath:
|
||||
'/generated-character-drafts/editor/asset-library/video/demo.mp4',
|
||||
formFields: {
|
||||
key: 'generated-character-drafts/editor/asset-library/video/demo.mp4',
|
||||
policy: 'policy',
|
||||
'x-oss-signature': 'signature',
|
||||
},
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
assetObject: {
|
||||
assetObjectId: 'assetobj-video',
|
||||
objectKey:
|
||||
'generated-character-drafts/editor/asset-library/video/demo.mp4',
|
||||
assetKind: 'editor_uploaded_video',
|
||||
},
|
||||
});
|
||||
getSignedAssetReadUrlMock.mockResolvedValueOnce(
|
||||
'https://signed.example.com/demo.mp4',
|
||||
);
|
||||
|
||||
const result = await uploadEditorMediaAssetFile(
|
||||
new File(['video'], 'demo.mp4', { type: 'video/mp4' }),
|
||||
'video',
|
||||
);
|
||||
|
||||
const ticketBody = JSON.parse(
|
||||
(requestJsonMock.mock.calls[0]?.[1] as RequestInit).body as string,
|
||||
);
|
||||
expect(ticketBody).toMatchObject({
|
||||
legacyPrefix: 'generated-character-drafts',
|
||||
pathSegments: ['editor', 'asset-library', 'video', '1771400000000'],
|
||||
fileName: 'demo.mp4',
|
||||
contentType: 'video/mp4',
|
||||
access: 'private',
|
||||
maxSizeBytes: 5,
|
||||
successActionStatus: 200,
|
||||
metadata: {
|
||||
asset_kind: 'editor_uploaded_video',
|
||||
editor_media_type: 'video',
|
||||
},
|
||||
});
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
'https://oss.example.com',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: expect.any(FormData),
|
||||
}),
|
||||
);
|
||||
const confirmBody = JSON.parse(
|
||||
(requestJsonMock.mock.calls[1]?.[1] as RequestInit).body as string,
|
||||
);
|
||||
expect(confirmBody).toMatchObject({
|
||||
bucket: 'bucket',
|
||||
objectKey: 'generated-character-drafts/editor/asset-library/video/demo.mp4',
|
||||
contentType: 'video/mp4',
|
||||
contentLength: 5,
|
||||
assetKind: 'editor_uploaded_video',
|
||||
accessPolicy: 'private',
|
||||
entityId: 'editor-asset-library-video',
|
||||
});
|
||||
expect(getSignedAssetReadUrlMock).toHaveBeenCalledWith(
|
||||
{
|
||||
objectKey:
|
||||
'generated-character-drafts/editor/asset-library/video/demo.mp4',
|
||||
expireSeconds: 3600,
|
||||
},
|
||||
undefined,
|
||||
{ bypassCache: true },
|
||||
);
|
||||
expect(result).toEqual({
|
||||
src: 'https://signed.example.com/demo.mp4',
|
||||
objectKey:
|
||||
'generated-character-drafts/editor/asset-library/video/demo.mp4',
|
||||
assetObjectId: 'assetobj-video',
|
||||
legacyPublicPath:
|
||||
'/generated-character-drafts/editor/asset-library/video/demo.mp4',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
import { requestJson } from '../apiClient';
|
||||
import { getSignedAssetReadUrl } from '../assetReadUrlService';
|
||||
|
||||
export type EditorMediaAssetUploadType = 'video' | 'audio';
|
||||
|
||||
type DirectUploadTicketResponse = {
|
||||
upload: {
|
||||
bucket: string;
|
||||
host: string;
|
||||
objectKey: string;
|
||||
legacyPublicPath: string;
|
||||
formFields: Record<string, string | null | undefined>;
|
||||
};
|
||||
};
|
||||
|
||||
type ConfirmAssetObjectResponse = {
|
||||
assetObject: {
|
||||
assetObjectId: string;
|
||||
objectKey: string;
|
||||
assetKind: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type UploadedEditorMediaAsset = {
|
||||
src: string;
|
||||
objectKey: string;
|
||||
assetObjectId: string;
|
||||
legacyPublicPath: string;
|
||||
};
|
||||
|
||||
const EDITOR_MEDIA_READ_EXPIRE_SECONDS = 60 * 60;
|
||||
|
||||
function resolveEditorMediaAssetKind(mediaType: EditorMediaAssetUploadType) {
|
||||
return mediaType === 'video'
|
||||
? 'editor_uploaded_video'
|
||||
: 'editor_uploaded_audio';
|
||||
}
|
||||
|
||||
function resolveFallbackFileName(mediaType: EditorMediaAssetUploadType) {
|
||||
return mediaType === 'video' ? 'editor-video.mp4' : 'editor-audio.mp3';
|
||||
}
|
||||
|
||||
function sanitizeEditorMediaFileName(
|
||||
fileName: string,
|
||||
mediaType: EditorMediaAssetUploadType,
|
||||
) {
|
||||
const trimmedName = fileName.trim();
|
||||
return trimmedName || resolveFallbackFileName(mediaType);
|
||||
}
|
||||
|
||||
async function postDirectUploadFile(
|
||||
upload: DirectUploadTicketResponse['upload'],
|
||||
file: File,
|
||||
) {
|
||||
const formData = new FormData();
|
||||
Object.entries(upload.formFields).forEach(([key, value]) => {
|
||||
if (value !== null && value !== undefined) {
|
||||
formData.append(key, value);
|
||||
}
|
||||
});
|
||||
formData.append('file', file, file.name);
|
||||
|
||||
const response = await fetch(upload.host, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('上传素材失败');
|
||||
}
|
||||
}
|
||||
|
||||
export async function uploadEditorMediaAssetFile(
|
||||
file: File,
|
||||
mediaType: EditorMediaAssetUploadType,
|
||||
): Promise<UploadedEditorMediaAsset> {
|
||||
const assetKind = resolveEditorMediaAssetKind(mediaType);
|
||||
const fileName = sanitizeEditorMediaFileName(file.name, mediaType);
|
||||
const contentType = file.type.trim() || 'application/octet-stream';
|
||||
|
||||
const ticket = await requestJson<DirectUploadTicketResponse>(
|
||||
'/api/assets/direct-upload-tickets',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
legacyPrefix: 'generated-character-drafts',
|
||||
pathSegments: ['editor', 'asset-library', mediaType, `${Date.now()}`],
|
||||
fileName,
|
||||
contentType,
|
||||
access: 'private',
|
||||
maxSizeBytes: file.size,
|
||||
successActionStatus: 200,
|
||||
metadata: {
|
||||
asset_kind: assetKind,
|
||||
editor_media_type: mediaType,
|
||||
},
|
||||
}),
|
||||
},
|
||||
'创建素材上传凭证失败',
|
||||
);
|
||||
|
||||
await postDirectUploadFile(ticket.upload, file);
|
||||
|
||||
const confirmed = await requestJson<ConfirmAssetObjectResponse>(
|
||||
'/api/assets/objects/confirm',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
bucket: ticket.upload.bucket,
|
||||
objectKey: ticket.upload.objectKey,
|
||||
contentType,
|
||||
contentLength: file.size,
|
||||
assetKind,
|
||||
accessPolicy: 'private',
|
||||
entityId: `editor-asset-library-${mediaType}`,
|
||||
}),
|
||||
},
|
||||
'确认素材资产失败',
|
||||
);
|
||||
|
||||
// 中文注释:素材库立即预览使用短期 signed URL;持久化仍记录 legacy path 和 objectKey。
|
||||
const signedUrl = await getSignedAssetReadUrl(
|
||||
{
|
||||
objectKey: confirmed.assetObject.objectKey,
|
||||
expireSeconds: EDITOR_MEDIA_READ_EXPIRE_SECONDS,
|
||||
},
|
||||
undefined,
|
||||
{ bypassCache: true },
|
||||
);
|
||||
|
||||
return {
|
||||
src: signedUrl,
|
||||
objectKey: confirmed.assetObject.objectKey,
|
||||
assetObjectId: confirmed.assetObject.assetObjectId,
|
||||
legacyPublicPath: ticket.upload.legacyPublicPath,
|
||||
};
|
||||
}
|
||||
@@ -279,6 +279,11 @@ describe('editorProjectClient', () => {
|
||||
prompt: 'dragon knight',
|
||||
model: 'gpt-image-2',
|
||||
sourceResourceId: 'resource-source',
|
||||
assetKind: 'character',
|
||||
generationInputs: {
|
||||
fields: [{ title: '角色设定', value: 'dragon knight' }],
|
||||
references: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
@@ -295,6 +300,11 @@ describe('editorProjectClient', () => {
|
||||
prompt: 'dragon knight',
|
||||
model: 'gpt-image-2',
|
||||
sourceResourceId: 'resource-source',
|
||||
assetKind: 'character',
|
||||
generationInputs: {
|
||||
fields: [{ title: '角色设定', value: 'dragon knight' }],
|
||||
references: [],
|
||||
},
|
||||
}),
|
||||
}),
|
||||
'创建图片画布资源失败',
|
||||
@@ -425,6 +435,11 @@ describe('editorProjectClient', () => {
|
||||
width: 640,
|
||||
height: 480,
|
||||
sourceType: 'uploaded',
|
||||
assetKind: 'icon',
|
||||
generationInputs: {
|
||||
fields: [{ title: '素材描述', value: '返回按钮' }],
|
||||
references: [],
|
||||
},
|
||||
});
|
||||
await updateEditorAsset('asset-1', {
|
||||
label: '角色主视觉.png',
|
||||
@@ -444,6 +459,11 @@ describe('editorProjectClient', () => {
|
||||
width: 640,
|
||||
height: 480,
|
||||
sourceType: 'uploaded',
|
||||
assetKind: 'icon',
|
||||
generationInputs: {
|
||||
fields: [{ title: '素材描述', value: '返回按钮' }],
|
||||
references: [],
|
||||
},
|
||||
}),
|
||||
}),
|
||||
'创建图片画布素材失败',
|
||||
@@ -551,6 +571,15 @@ describe('editorProjectClient', () => {
|
||||
model: 'gpt-image-2',
|
||||
aspectRatio: '2:3',
|
||||
imageSize: '1K',
|
||||
projectId: 'editor-project-1',
|
||||
assetKind: 'character',
|
||||
assetFolderId: 'project',
|
||||
assetLabel: '角色形象',
|
||||
sourceResourceId: 'resource-spec-1',
|
||||
generationInputs: {
|
||||
fields: [{ title: '角色设定', value: '角色设定' }],
|
||||
references: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
@@ -564,6 +593,15 @@ describe('editorProjectClient', () => {
|
||||
model: 'gpt-image-2',
|
||||
aspectRatio: '2:3',
|
||||
imageSize: '1K',
|
||||
projectId: 'editor-project-1',
|
||||
assetKind: 'character',
|
||||
generationInputs: {
|
||||
fields: [{ title: '角色设定', value: '角色设定' }],
|
||||
references: [],
|
||||
},
|
||||
assetFolderId: 'project',
|
||||
assetLabel: '角色形象',
|
||||
sourceResourceId: 'resource-spec-1',
|
||||
}),
|
||||
}),
|
||||
'生成图片失败',
|
||||
@@ -643,6 +681,12 @@ describe('editorProjectClient', () => {
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '2K',
|
||||
priceMudPoints: 20,
|
||||
projectId: 'editor-project-1',
|
||||
assetFolderId: 'project',
|
||||
generationInputs: {
|
||||
fields: [{ title: '素材描述', value: '返回按钮' }],
|
||||
references: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
@@ -657,6 +701,12 @@ describe('editorProjectClient', () => {
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '2K',
|
||||
priceMudPoints: 20,
|
||||
projectId: 'editor-project-1',
|
||||
generationInputs: {
|
||||
fields: [{ title: '素材描述', value: '返回按钮' }],
|
||||
references: [],
|
||||
},
|
||||
assetFolderId: 'project',
|
||||
}),
|
||||
}),
|
||||
'生成图标素材失败',
|
||||
@@ -690,6 +740,13 @@ describe('editorProjectClient', () => {
|
||||
|
||||
const result = await extractEditorUiDesignAssets({
|
||||
sourceImageSrc: 'data:image/png;base64,ui-design',
|
||||
projectId: 'editor-project-1',
|
||||
assetFolderId: 'project',
|
||||
spritesheetLabel: '战斗UI设计图 素材图集',
|
||||
generationInputs: {
|
||||
fields: [{ title: '来源', value: '战斗UI设计图' }],
|
||||
references: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.taskId).toBe('ui-asset-extraction-1');
|
||||
@@ -700,6 +757,13 @@ describe('editorProjectClient', () => {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
sourceImageSrc: 'data:image/png;base64,ui-design',
|
||||
projectId: 'editor-project-1',
|
||||
generationInputs: {
|
||||
fields: [{ title: '来源', value: '战斗UI设计图' }],
|
||||
references: [],
|
||||
},
|
||||
assetFolderId: 'project',
|
||||
spritesheetLabel: '战斗UI设计图 素材图集',
|
||||
}),
|
||||
}),
|
||||
'提取UI设计图素材失败',
|
||||
@@ -768,6 +832,8 @@ describe('editorProjectClient', () => {
|
||||
prompt: '宣发素材 prompt',
|
||||
size: '720x1280',
|
||||
kind: 'publication-material',
|
||||
aspectRatio: '9:16',
|
||||
imageSize: '1K',
|
||||
referenceImageSrcs: ['data:image/png;base64,ref'],
|
||||
});
|
||||
|
||||
@@ -781,6 +847,8 @@ describe('editorProjectClient', () => {
|
||||
prompt: '宣发素材 prompt',
|
||||
size: '720x1280',
|
||||
kind: 'publication-material',
|
||||
aspectRatio: '9:16',
|
||||
imageSize: '1K',
|
||||
referenceImageSrcs: ['data:image/png;base64,ref'],
|
||||
}),
|
||||
}),
|
||||
@@ -1108,6 +1176,15 @@ describe('editorProjectClient', () => {
|
||||
sourceImageSrc: 'data:image/png;base64,source',
|
||||
size: '1024x1024',
|
||||
model: 'gpt-image-2',
|
||||
projectId: 'editor-project-1',
|
||||
assetKind: 'character',
|
||||
assetFolderId: 'project',
|
||||
assetLabel: '角色形象 修改结果',
|
||||
sourceResourceId: 'resource-character-1',
|
||||
generationInputs: {
|
||||
fields: [{ title: '修改提示词', value: '把画面改成黄昏光线' }],
|
||||
references: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.taskId).toBe('vector-edit-1');
|
||||
@@ -1121,6 +1198,15 @@ describe('editorProjectClient', () => {
|
||||
sourceImageSrc: 'data:image/png;base64,source',
|
||||
size: '1024x1024',
|
||||
model: 'gpt-image-2',
|
||||
projectId: 'editor-project-1',
|
||||
assetKind: 'character',
|
||||
generationInputs: {
|
||||
fields: [{ title: '修改提示词', value: '把画面改成黄昏光线' }],
|
||||
references: [],
|
||||
},
|
||||
assetFolderId: 'project',
|
||||
assetLabel: '角色形象 修改结果',
|
||||
sourceResourceId: 'resource-character-1',
|
||||
}),
|
||||
}),
|
||||
'修改图片失败',
|
||||
|
||||
@@ -29,6 +29,22 @@ export type EditorProjectLayerSnapshot = Record<string, unknown> & {
|
||||
resourceId: string;
|
||||
};
|
||||
|
||||
export type EditorAssetGenerationInputField = {
|
||||
title: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type EditorAssetGenerationInputReference = {
|
||||
title: string;
|
||||
label: string;
|
||||
src: string;
|
||||
};
|
||||
|
||||
export type EditorAssetGenerationInputs = {
|
||||
fields: EditorAssetGenerationInputField[];
|
||||
references: EditorAssetGenerationInputReference[];
|
||||
};
|
||||
|
||||
export type EditorProjectResourceSourceType =
|
||||
| 'uploaded'
|
||||
| 'generated'
|
||||
@@ -50,6 +66,8 @@ export type EditorProjectResourceSnapshot = {
|
||||
taskId?: string | null;
|
||||
durationSeconds?: number | null;
|
||||
sourceResourceId?: string | null;
|
||||
assetKind?: string | null;
|
||||
generationInputs?: EditorAssetGenerationInputs | null;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
};
|
||||
@@ -80,6 +98,8 @@ export type EditorAssetSnapshot = {
|
||||
provider?: string | null;
|
||||
taskId?: string | null;
|
||||
durationSeconds?: number | null;
|
||||
assetKind?: string | null;
|
||||
generationInputs?: EditorAssetGenerationInputs | null;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
};
|
||||
@@ -102,6 +122,12 @@ export type EditorImageGenerationInput = {
|
||||
aspectRatio?: string;
|
||||
imageSize?: string;
|
||||
referenceImageSrcs?: string[];
|
||||
projectId?: string | null;
|
||||
assetKind?: string | null;
|
||||
generationInputs?: EditorAssetGenerationInputs | null;
|
||||
assetFolderId?: string | null;
|
||||
assetLabel?: string | null;
|
||||
sourceResourceId?: string | null;
|
||||
};
|
||||
|
||||
export type EditorIconSpritesheetGenerationInput = {
|
||||
@@ -111,10 +137,17 @@ export type EditorIconSpritesheetGenerationInput = {
|
||||
aspectRatio?: string;
|
||||
imageSize?: string;
|
||||
priceMudPoints: number;
|
||||
projectId?: string | null;
|
||||
generationInputs?: EditorAssetGenerationInputs | null;
|
||||
assetFolderId?: string | null;
|
||||
};
|
||||
|
||||
export type EditorUiDesignAssetExtractionInput = {
|
||||
sourceImageSrc: string;
|
||||
projectId?: string | null;
|
||||
generationInputs?: EditorAssetGenerationInputs | null;
|
||||
assetFolderId?: string | null;
|
||||
spritesheetLabel?: string | null;
|
||||
};
|
||||
|
||||
export type EditorImageEditInput = {
|
||||
@@ -122,6 +155,12 @@ export type EditorImageEditInput = {
|
||||
sourceImageSrc: string;
|
||||
size?: string;
|
||||
model?: string;
|
||||
projectId?: string | null;
|
||||
assetKind?: string | null;
|
||||
generationInputs?: EditorAssetGenerationInputs | null;
|
||||
assetFolderId?: string | null;
|
||||
assetLabel?: string | null;
|
||||
sourceResourceId?: string | null;
|
||||
};
|
||||
|
||||
export type EditorImageGenerationResult = {
|
||||
@@ -136,6 +175,8 @@ export type EditorImageGenerationResult = {
|
||||
model: string;
|
||||
provider: string;
|
||||
taskId: string;
|
||||
resource?: EditorProjectResourceSnapshot | null;
|
||||
asset?: EditorAssetSnapshot | null;
|
||||
};
|
||||
|
||||
export type EditorIconSpritesheetIconResult = {
|
||||
@@ -143,6 +184,8 @@ export type EditorIconSpritesheetIconResult = {
|
||||
imageSrc: string;
|
||||
width: number;
|
||||
height: number;
|
||||
resource?: EditorProjectResourceSnapshot | null;
|
||||
asset?: EditorAssetSnapshot | null;
|
||||
};
|
||||
|
||||
export type EditorIconSpritesheetGenerationResult = {
|
||||
@@ -156,6 +199,8 @@ export type EditorIconSpritesheetGenerationResult = {
|
||||
provider: string;
|
||||
taskId: string;
|
||||
priceMudPoints: number;
|
||||
spritesheetResource?: EditorProjectResourceSnapshot | null;
|
||||
spritesheetAsset?: EditorAssetSnapshot | null;
|
||||
};
|
||||
|
||||
export type EditorCharacterAnimationResolution = '480p' | '720p';
|
||||
@@ -229,6 +274,8 @@ export type EditorVideoGenerationInput = {
|
||||
|
||||
export type EditorVideoGenerationResult = {
|
||||
videoSrc: string;
|
||||
objectKey?: string | null;
|
||||
assetObjectId?: string | null;
|
||||
width: number;
|
||||
height: number;
|
||||
sourceType: 'generated';
|
||||
@@ -255,6 +302,8 @@ export type EditorBackgroundMusicGenerationInput = {
|
||||
|
||||
export type EditorAudioGenerationResult = {
|
||||
audioSrc: string;
|
||||
objectKey?: string | null;
|
||||
assetObjectId?: string | null;
|
||||
width: number;
|
||||
height: number;
|
||||
sourceType: 'generated';
|
||||
@@ -311,6 +360,8 @@ export type EditorProjectResourceCreateInput = {
|
||||
taskId?: string | null;
|
||||
durationSeconds?: number | null;
|
||||
sourceResourceId?: string | null;
|
||||
assetKind?: string | null;
|
||||
generationInputs?: EditorAssetGenerationInputs | null;
|
||||
};
|
||||
|
||||
export type EditorAssetCreateInput = {
|
||||
@@ -328,6 +379,8 @@ export type EditorAssetCreateInput = {
|
||||
provider?: string | null;
|
||||
taskId?: string | null;
|
||||
durationSeconds?: number | null;
|
||||
assetKind?: string | null;
|
||||
generationInputs?: EditorAssetGenerationInputs | null;
|
||||
};
|
||||
|
||||
export type EditorAssetUpdateInput = {
|
||||
@@ -554,6 +607,16 @@ export async function generateEditorImage(input: EditorImageGenerationInput) {
|
||||
...(input.referenceImageSrcs?.length
|
||||
? { referenceImageSrcs: input.referenceImageSrcs }
|
||||
: {}),
|
||||
...(input.projectId ? { projectId: input.projectId } : {}),
|
||||
...(input.assetKind ? { assetKind: input.assetKind } : {}),
|
||||
...(input.generationInputs
|
||||
? { generationInputs: input.generationInputs }
|
||||
: {}),
|
||||
...(input.assetFolderId ? { assetFolderId: input.assetFolderId } : {}),
|
||||
...(input.assetLabel ? { assetLabel: input.assetLabel } : {}),
|
||||
...(input.sourceResourceId
|
||||
? { sourceResourceId: input.sourceResourceId }
|
||||
: {}),
|
||||
}),
|
||||
'生成图片失败',
|
||||
{
|
||||
@@ -577,6 +640,11 @@ export async function generateEditorIconSpritesheet(
|
||||
...(input.aspectRatio ? { aspectRatio: input.aspectRatio } : {}),
|
||||
...(input.imageSize ? { imageSize: input.imageSize } : {}),
|
||||
priceMudPoints: input.priceMudPoints,
|
||||
...(input.projectId ? { projectId: input.projectId } : {}),
|
||||
...(input.generationInputs
|
||||
? { generationInputs: input.generationInputs }
|
||||
: {}),
|
||||
...(input.assetFolderId ? { assetFolderId: input.assetFolderId } : {}),
|
||||
}),
|
||||
'生成图标素材失败',
|
||||
{
|
||||
@@ -595,6 +663,14 @@ export async function extractEditorUiDesignAssets(
|
||||
EDITOR_UI_DESIGN_ASSET_EXTRACTION_API,
|
||||
jsonRequest('POST', {
|
||||
sourceImageSrc: input.sourceImageSrc,
|
||||
...(input.projectId ? { projectId: input.projectId } : {}),
|
||||
...(input.generationInputs
|
||||
? { generationInputs: input.generationInputs }
|
||||
: {}),
|
||||
...(input.assetFolderId ? { assetFolderId: input.assetFolderId } : {}),
|
||||
...(input.spritesheetLabel
|
||||
? { spritesheetLabel: input.spritesheetLabel }
|
||||
: {}),
|
||||
}),
|
||||
'提取UI设计图素材失败',
|
||||
{
|
||||
@@ -614,6 +690,16 @@ export async function editEditorImage(input: EditorImageEditInput) {
|
||||
sourceImageSrc: input.sourceImageSrc,
|
||||
...(input.size ? { size: input.size } : {}),
|
||||
...(input.model ? { model: input.model } : {}),
|
||||
...(input.projectId ? { projectId: input.projectId } : {}),
|
||||
...(input.assetKind ? { assetKind: input.assetKind } : {}),
|
||||
...(input.generationInputs
|
||||
? { generationInputs: input.generationInputs }
|
||||
: {}),
|
||||
...(input.assetFolderId ? { assetFolderId: input.assetFolderId } : {}),
|
||||
...(input.assetLabel ? { assetLabel: input.assetLabel } : {}),
|
||||
...(input.sourceResourceId
|
||||
? { sourceResourceId: input.sourceResourceId }
|
||||
: {}),
|
||||
}),
|
||||
'修改图片失败',
|
||||
{
|
||||
|
||||
@@ -7,8 +7,11 @@ const { fetchWithApiAuthMock, requestJsonMock } = vi.hoisted(() => ({
|
||||
|
||||
import {
|
||||
clearRpgProfileBrowseHistory,
|
||||
createRpgProfileExternalApiKey,
|
||||
listRpgProfileBrowseHistory,
|
||||
listRpgProfileExternalApiKeys,
|
||||
listRpgProfileSaveArchives,
|
||||
revokeRpgProfileExternalApiKey,
|
||||
resumeRpgProfileSaveArchive,
|
||||
submitRpgProfileFeedback,
|
||||
syncRpgProfileBrowseHistory,
|
||||
@@ -199,6 +202,64 @@ describe('rpgProfileClient save archive routes', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('rpgProfileClient external api key routes', () => {
|
||||
beforeEach(() => {
|
||||
requestJsonMock.mockReset();
|
||||
requestJsonMock.mockResolvedValue({ keys: [] });
|
||||
});
|
||||
|
||||
it('lists profile api keys from the profile route', async () => {
|
||||
await listRpgProfileExternalApiKeys();
|
||||
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
'/api/profile/api-keys',
|
||||
expect.objectContaining({ method: 'GET' }),
|
||||
'读取 API Key 失败',
|
||||
expect.objectContaining({
|
||||
retry: expect.objectContaining({ maxRetries: 1 }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('creates profile api keys through the profile route', async () => {
|
||||
await createRpgProfileExternalApiKey('测试 Key');
|
||||
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
'/api/profile/api-keys',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
'创建 API Key 失败',
|
||||
expect.objectContaining({
|
||||
retry: expect.objectContaining({
|
||||
maxRetries: 1,
|
||||
retryUnsafeMethods: true,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(JSON.parse(requestJsonMock.mock.calls[0][1].body)).toEqual({
|
||||
name: '测试 Key',
|
||||
});
|
||||
});
|
||||
|
||||
it('revokes profile api keys through the profile route', async () => {
|
||||
await revokeRpgProfileExternalApiKey('key:1');
|
||||
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
'/api/profile/api-keys/key%3A1',
|
||||
expect.objectContaining({ method: 'DELETE' }),
|
||||
'撤销 API Key 失败',
|
||||
expect.objectContaining({
|
||||
retry: expect.objectContaining({
|
||||
maxRetries: 1,
|
||||
retryUnsafeMethods: true,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rpgProfileClient feedback routes', () => {
|
||||
beforeEach(() => {
|
||||
requestJsonMock.mockReset();
|
||||
|
||||
@@ -2,6 +2,9 @@ import type {
|
||||
ClaimProfileTaskRewardResponse,
|
||||
ConfirmWechatProfileRechargeOrderResponse,
|
||||
CreateProfileRechargeOrderResponse,
|
||||
ExternalApiKeyCreateResponse,
|
||||
ExternalApiKeyListResponse,
|
||||
ExternalApiKeyMutationResponse,
|
||||
PlatformBrowseHistoryBatchSyncRequest,
|
||||
PlatformBrowseHistoryResponse,
|
||||
PlatformBrowseHistoryWriteEntry,
|
||||
@@ -79,6 +82,45 @@ export function getRpgProfileWalletLedger(options: RuntimeRequestOptions = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
export function listRpgProfileExternalApiKeys(
|
||||
options: RuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestRpgRuntimeJson<ExternalApiKeyListResponse>(
|
||||
'/profile/api-keys',
|
||||
{ method: 'GET' },
|
||||
'读取 API Key 失败',
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
export function createRpgProfileExternalApiKey(
|
||||
name: string,
|
||||
options: RuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestRpgRuntimeJson<ExternalApiKeyCreateResponse>(
|
||||
'/profile/api-keys',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name }),
|
||||
},
|
||||
'创建 API Key 失败',
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
export function revokeRpgProfileExternalApiKey(
|
||||
keyId: string,
|
||||
options: RuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestRpgRuntimeJson<ExternalApiKeyMutationResponse>(
|
||||
`/profile/api-keys/${encodeURIComponent(keyId)}`,
|
||||
{ method: 'DELETE' },
|
||||
'撤销 API Key 失败',
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
export function getRpgProfileRechargeCenter(
|
||||
options: RuntimeRequestOptions = {},
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user