1
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
putCharacterRoleAssetWorkflow,
|
||||
resolveCharacterRoleAssetWorkflow,
|
||||
} from './characterAssetWorkflowPersistence';
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe('角色资产工坊 workflow client', () => {
|
||||
it('通过后端 workflow 接口解析默认 prompt 和缓存合并结果', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
text: async () =>
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
cache: null,
|
||||
workflow: {
|
||||
defaultPromptBundle: {
|
||||
visualPromptText: '默认视觉',
|
||||
animationPromptText: '默认动作',
|
||||
scenePromptText: '默认场景',
|
||||
},
|
||||
visualPromptText: '默认视觉',
|
||||
animationPromptText: '默认动作',
|
||||
animationPromptTextByKey: { run: '默认动作' },
|
||||
visualDrafts: [],
|
||||
selectedVisualDraftId: '',
|
||||
selectedAnimation: 'run',
|
||||
},
|
||||
}),
|
||||
});
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
const result = await resolveCharacterRoleAssetWorkflow({
|
||||
characterId: 'role 01',
|
||||
cacheScopeId: 'world-01',
|
||||
role: {
|
||||
id: 'role 01',
|
||||
name: '沈砺',
|
||||
title: '灰炬向导',
|
||||
role: '边路同行者',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.workflow.visualPromptText).toBe('默认视觉');
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/runtime/custom-world/asset-studio/role/role%2001/workflow',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
cacheScopeId: 'world-01',
|
||||
role: {
|
||||
id: 'role 01',
|
||||
name: '沈砺',
|
||||
title: '灰炬向导',
|
||||
role: '边路同行者',
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('使用 PUT 保存用户当前工坊草稿缓存', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
text: async () =>
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
cache: { characterId: 'role-01' },
|
||||
saveMessage: '已保存',
|
||||
}),
|
||||
});
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
await putCharacterRoleAssetWorkflow({
|
||||
characterId: 'role-01',
|
||||
cacheScopeId: 'world-01',
|
||||
visualPromptText: '视觉草稿',
|
||||
animationPromptText: '动作草稿',
|
||||
animationPromptTextByKey: { run: '奔跑草稿' },
|
||||
visualDrafts: [],
|
||||
selectedVisualDraftId: '',
|
||||
selectedAnimation: 'run',
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/runtime/custom-world/asset-studio/role/role-01/workflow',
|
||||
expect.objectContaining({
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
characterId: 'role-01',
|
||||
cacheScopeId: 'world-01',
|
||||
visualPromptText: '视觉草稿',
|
||||
animationPromptText: '动作草稿',
|
||||
animationPromptTextByKey: { run: '奔跑草稿' },
|
||||
visualDrafts: [],
|
||||
selectedVisualDraftId: '',
|
||||
selectedAnimation: 'run',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,10 @@ import {
|
||||
ASSET_API_PATHS,
|
||||
postApiJson,
|
||||
} from '../../editor/shared/editorApiClient';
|
||||
import { fetchJson } from '../../editor/shared/jsonClient';
|
||||
import {
|
||||
fetchJson,
|
||||
parseApiErrorMessage,
|
||||
} from '../../editor/shared/jsonClient';
|
||||
|
||||
export const CHARACTER_VISUAL_GENERATE_API_PATH =
|
||||
ASSET_API_PATHS.characterVisualGenerate;
|
||||
@@ -21,6 +24,8 @@ export const CHARACTER_ANIMATION_IMPORT_VIDEO_API_PATH =
|
||||
ASSET_API_PATHS.characterAnimationImportVideo;
|
||||
export const CHARACTER_ANIMATION_TEMPLATES_API_PATH =
|
||||
ASSET_API_PATHS.characterAnimationTemplates;
|
||||
export const ROLE_ASSET_WORKFLOW_API_PATH =
|
||||
'/api/runtime/custom-world/asset-studio/role';
|
||||
|
||||
export type CharacterVisualSourceMode =
|
||||
| 'text-to-image'
|
||||
@@ -61,6 +66,48 @@ export type CharacterAssetWorkflowCache = {
|
||||
updatedAt?: string;
|
||||
};
|
||||
|
||||
export type CharacterAssetRolePromptInput = {
|
||||
id: string;
|
||||
name?: string;
|
||||
title?: string;
|
||||
role?: string;
|
||||
visualDescription?: string;
|
||||
actionDescription?: string;
|
||||
sceneVisualDescription?: string;
|
||||
description?: string;
|
||||
backstory?: string;
|
||||
personality?: string;
|
||||
motivation?: string;
|
||||
combatStyle?: string;
|
||||
tags?: string[];
|
||||
imageSrc?: string;
|
||||
generatedVisualAssetId?: string;
|
||||
generatedAnimationSetId?: string;
|
||||
animationMap?: Record<string, unknown> | null;
|
||||
};
|
||||
|
||||
export type CharacterRolePromptBundle = {
|
||||
visualPromptText: string;
|
||||
animationPromptText: string;
|
||||
scenePromptText: string;
|
||||
};
|
||||
|
||||
export type CharacterRoleAssetWorkflow = {
|
||||
role: CharacterAssetRolePromptInput;
|
||||
defaultPromptBundle: CharacterRolePromptBundle;
|
||||
visualPromptText: string;
|
||||
animationPromptText: string;
|
||||
animationPromptTextByKey: Record<string, string>;
|
||||
visualDrafts: CharacterVisualDraft[];
|
||||
selectedVisualDraftId: string;
|
||||
selectedAnimation: string;
|
||||
imageSrc?: string;
|
||||
generatedVisualAssetId?: string;
|
||||
generatedAnimationSetId?: string;
|
||||
animationMap?: Record<string, unknown> | null;
|
||||
updatedAt?: string;
|
||||
};
|
||||
|
||||
export type CharacterVisualGenerationPayload = {
|
||||
characterId: string;
|
||||
sourceMode: Exclude<CharacterVisualSourceMode, 'upload'>;
|
||||
@@ -185,6 +232,60 @@ export async function saveCharacterWorkflowCache(
|
||||
);
|
||||
}
|
||||
|
||||
export async function resolveCharacterRoleAssetWorkflow(payload: {
|
||||
characterId: string;
|
||||
cacheScopeId?: string;
|
||||
role: CharacterAssetRolePromptInput;
|
||||
}) {
|
||||
const { characterId, cacheScopeId, role } = payload;
|
||||
return postApiJson<{
|
||||
ok: true;
|
||||
cache: CharacterAssetWorkflowCache | null;
|
||||
workflow: CharacterRoleAssetWorkflow;
|
||||
}>(
|
||||
`${ROLE_ASSET_WORKFLOW_API_PATH}/${encodeURIComponent(characterId)}/workflow`,
|
||||
{
|
||||
cacheScopeId,
|
||||
role,
|
||||
},
|
||||
'读取角色资产工坊工作流失败',
|
||||
);
|
||||
}
|
||||
|
||||
export async function putCharacterRoleAssetWorkflow(
|
||||
payload: CharacterAssetWorkflowCache,
|
||||
) {
|
||||
const url = `${ROLE_ASSET_WORKFLOW_API_PATH}/${encodeURIComponent(payload.characterId)}/workflow`;
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const responseText = await response.text();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
parseApiErrorMessage(responseText, '保存角色资产工坊缓存失败'),
|
||||
);
|
||||
}
|
||||
|
||||
return responseText
|
||||
? (JSON.parse(responseText) as {
|
||||
ok: true;
|
||||
cache: CharacterAssetWorkflowCache;
|
||||
saveMessage: string;
|
||||
})
|
||||
: ({
|
||||
ok: true,
|
||||
cache: payload,
|
||||
saveMessage: '',
|
||||
} as {
|
||||
ok: true;
|
||||
cache: CharacterAssetWorkflowCache;
|
||||
saveMessage: string;
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchCharacterVisualJobStatus(taskId: string) {
|
||||
return fetchJson<CharacterAssetJobStatus>(
|
||||
`${CHARACTER_VISUAL_JOB_API_PATH}/${encodeURIComponent(taskId)}`,
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { buildDefaultRolePromptBundle } from './customWorldRolePromptDefaults';
|
||||
|
||||
describe('buildDefaultRolePromptBundle', () => {
|
||||
it('uses model-generated role descriptions directly', () => {
|
||||
const result = buildDefaultRolePromptBundle({
|
||||
name: '沈砺',
|
||||
title: '灰炬向导',
|
||||
role: '边路同行者',
|
||||
visualDescription:
|
||||
'灰黑短斗篷压着风痕,肩侧挂着旧路标与短弓,整个人像常年在裂潮边路里行走的人。',
|
||||
actionDescription:
|
||||
'起手先观察风向和站位,再用短弓牵制后迅速贴近补刀,动作克制但很准。',
|
||||
sceneVisualDescription:
|
||||
'他常出现的边路哨点铺着潮湿石板,旧灯火和风旗一直在晃,空气里带着将散未散的盐雾。',
|
||||
description: '熟悉裂潮边路的灰炬向导。',
|
||||
});
|
||||
|
||||
expect(result.visualPromptText).toBe(
|
||||
'灰黑短斗篷压着风痕,肩侧挂着旧路标与短弓,整个人像常年在裂潮边路里行走的人。',
|
||||
);
|
||||
expect(result.animationPromptText).toBe(
|
||||
'起手先观察风向和站位,再用短弓牵制后迅速贴近补刀,动作克制但很准。',
|
||||
);
|
||||
expect(result.scenePromptText).toBe(
|
||||
'他常出现的边路哨点铺着潮湿石板,旧灯火和风旗一直在晃,空气里带着将散未散的盐雾。',
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to existing entity descriptions without assembling new rules', () => {
|
||||
const result = buildDefaultRolePromptBundle({
|
||||
name: '顾潮音',
|
||||
title: '港口守望者',
|
||||
role: '场景角色',
|
||||
description: '总在潮雾港高处盯着来往船影的守望者。',
|
||||
personality: '寡言、敏锐、先看人再开口。',
|
||||
combatStyle: '长枪封线后借高差压制。',
|
||||
motivation: '想在港口旧秩序彻底崩掉前找出新的站位。',
|
||||
backstory: '他把许多没说出口的旧案痕迹留在港口高处。',
|
||||
tags: ['潮雾港', '守望', '旧案'],
|
||||
});
|
||||
|
||||
expect(result.visualPromptText).toBe('总在潮雾港高处盯着来往船影的守望者。');
|
||||
expect(result.animationPromptText).toBe('长枪封线后借高差压制。');
|
||||
expect(result.scenePromptText).toBe('他把许多没说出口的旧案痕迹留在港口高处。');
|
||||
expect(result.visualPromptText).not.toContain('经典横版像素动作角色');
|
||||
expect(result.visualPromptText).not.toContain('深色粗轮廓配合清晰大色块');
|
||||
expect(result.visualPromptText).not.toContain('提示词');
|
||||
});
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
export * from '../../prompts/customWorldRolePromptDefaults';
|
||||
Reference in New Issue
Block a user