feat: add edutainment drawing and visual package flows
This commit is contained in:
102
src/services/edutainment-baby-drawing/babyDrawingClient.test.ts
Normal file
102
src/services/edutainment-baby-drawing/babyDrawingClient.test.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { requestJson } from '../apiClient';
|
||||
import {
|
||||
createBabyLoveDrawingMagicImage,
|
||||
listLocalBabyLoveDrawings,
|
||||
saveBabyLoveDrawing,
|
||||
} from './babyDrawingClient';
|
||||
|
||||
vi.mock('../apiClient', () => ({
|
||||
requestJson: vi.fn(),
|
||||
}));
|
||||
|
||||
const requestJsonMock = vi.mocked(requestJson);
|
||||
|
||||
describe('babyDrawingClient', () => {
|
||||
beforeEach(() => {
|
||||
const store = new Map<string, string>();
|
||||
vi.stubGlobal('window', {
|
||||
localStorage: {
|
||||
getItem: (key: string) => store.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => {
|
||||
store.set(key, value);
|
||||
},
|
||||
},
|
||||
});
|
||||
vi.stubGlobal('crypto', {
|
||||
randomUUID: () => '11111111-2222-3333-4444-555555555555',
|
||||
});
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-05-13T08:00:00.000Z'));
|
||||
requestJsonMock.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
test('saves original drawing only when magic image is absent', () => {
|
||||
const response = saveBabyLoveDrawing({
|
||||
originalImageSrc: 'data:image/png;base64,original',
|
||||
magicImageSrc: null,
|
||||
strokeTrace: [],
|
||||
});
|
||||
|
||||
expect(response.record).toMatchObject({
|
||||
templateName: '宝贝爱画',
|
||||
originalImageSrc: 'data:image/png;base64,original',
|
||||
magicImageSrc: null,
|
||||
saveMode: 'original-only',
|
||||
themeTags: ['寓教于乐', '宝贝爱画'],
|
||||
});
|
||||
expect(listLocalBabyLoveDrawings()).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('saves original and magic image together after magic generation', () => {
|
||||
const response = saveBabyLoveDrawing({
|
||||
originalImageSrc: 'data:image/png;base64,original',
|
||||
magicImageSrc: 'data:image/png;base64,magic',
|
||||
strokeTrace: [
|
||||
{
|
||||
strokeId: 'stroke-1',
|
||||
tool: 'brush',
|
||||
color: '#ef4444',
|
||||
points: [{ x: 0.1, y: 0.2, t: 1 }],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(response.record.saveMode).toBe('original-and-magic');
|
||||
expect(response.record.magicImageSrc).toBe('data:image/png;base64,magic');
|
||||
expect(listLocalBabyLoveDrawings()[0]?.strokeTrace).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('creates magic image through backend image-2 proxy', async () => {
|
||||
requestJsonMock.mockResolvedValueOnce({
|
||||
magicImageSrc: 'data:image/png;base64,magic',
|
||||
generationProvider: 'vector-engine-gpt-image-2',
|
||||
prompt: '绘本风格',
|
||||
});
|
||||
|
||||
const payload = {
|
||||
originalImageSrc: 'data:image/png;base64,original',
|
||||
strokeTrace: [],
|
||||
};
|
||||
const response = await createBabyLoveDrawingMagicImage(payload);
|
||||
|
||||
expect(response.magicImageSrc).toContain('magic');
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
'/api/creation/edutainment/baby-love-drawing/magic',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
'生成宝贝爱画魔法图片失败',
|
||||
expect.objectContaining({
|
||||
timeoutMs: 180000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
128
src/services/edutainment-baby-drawing/babyDrawingClient.ts
Normal file
128
src/services/edutainment-baby-drawing/babyDrawingClient.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import type {
|
||||
BabyLoveDrawingRecord,
|
||||
CreateBabyLoveDrawingMagicRequest,
|
||||
CreateBabyLoveDrawingMagicResponse,
|
||||
SaveBabyLoveDrawingRequest,
|
||||
SaveBabyLoveDrawingResponse,
|
||||
} from '../../../packages/shared/src/contracts/edutainmentBabyDrawing';
|
||||
import {
|
||||
BABY_LOVE_DRAWING_EDUTAINMENT_TAG,
|
||||
BABY_LOVE_DRAWING_TEMPLATE_ID,
|
||||
BABY_LOVE_DRAWING_TEMPLATE_NAME,
|
||||
normalizeBabyLoveDrawingTags,
|
||||
} from '../../../packages/shared/src/contracts/edutainmentBabyDrawing';
|
||||
import { type ApiRetryOptions, requestJson } from '../apiClient';
|
||||
|
||||
const STORAGE_KEY = 'genarrative.edutainmentBabyDrawing.localDrawings.v1';
|
||||
const BABY_LOVE_DRAWING_MAGIC_API =
|
||||
'/api/creation/edutainment/baby-love-drawing/magic';
|
||||
const BABY_LOVE_DRAWING_MAGIC_RETRY: ApiRetryOptions = {
|
||||
maxRetries: 1,
|
||||
baseDelayMs: 800,
|
||||
maxDelayMs: 2000,
|
||||
retryUnsafeMethods: true,
|
||||
};
|
||||
|
||||
type LocalDrawingStore = Record<string, BabyLoveDrawingRecord>;
|
||||
|
||||
function canUseLocalStorage() {
|
||||
return (
|
||||
typeof window !== 'undefined' && typeof window.localStorage !== 'undefined'
|
||||
);
|
||||
}
|
||||
|
||||
function readLocalDrawingStore(): LocalDrawingStore {
|
||||
if (!canUseLocalStorage()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
const rawValue = window.localStorage.getItem(STORAGE_KEY);
|
||||
if (!rawValue) {
|
||||
return {};
|
||||
}
|
||||
const parsed = JSON.parse(rawValue) as LocalDrawingStore;
|
||||
return parsed && typeof parsed === 'object' ? parsed : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function writeLocalDrawingStore(store: LocalDrawingStore) {
|
||||
if (!canUseLocalStorage()) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(store));
|
||||
}
|
||||
|
||||
function createLocalId(prefix: string) {
|
||||
const randomPart =
|
||||
typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
||||
? crypto.randomUUID().replace(/-/gu, '')
|
||||
: Math.random().toString(36).slice(2);
|
||||
|
||||
return `${prefix}-${Date.now().toString(36)}-${randomPart.slice(0, 12)}`;
|
||||
}
|
||||
|
||||
function saveRecordToLocalStore(record: BabyLoveDrawingRecord) {
|
||||
const store = readLocalDrawingStore();
|
||||
store[record.drawingId] = record;
|
||||
writeLocalDrawingStore(store);
|
||||
}
|
||||
|
||||
export function listLocalBabyLoveDrawings() {
|
||||
return Object.values(readLocalDrawingStore()).sort(
|
||||
(left, right) =>
|
||||
new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime(),
|
||||
);
|
||||
}
|
||||
|
||||
export function saveBabyLoveDrawing(
|
||||
payload: SaveBabyLoveDrawingRequest,
|
||||
): SaveBabyLoveDrawingResponse {
|
||||
const now = new Date().toISOString();
|
||||
const magicImageSrc = payload.magicImageSrc?.trim() || null;
|
||||
const record: BabyLoveDrawingRecord = {
|
||||
drawingId: createLocalId('baby-love-drawing'),
|
||||
templateId: BABY_LOVE_DRAWING_TEMPLATE_ID,
|
||||
templateName: BABY_LOVE_DRAWING_TEMPLATE_NAME,
|
||||
originalImageSrc: payload.originalImageSrc,
|
||||
magicImageSrc,
|
||||
strokeTrace: payload.strokeTrace,
|
||||
saveMode: magicImageSrc ? 'original-and-magic' : 'original-only',
|
||||
themeTags: normalizeBabyLoveDrawingTags([
|
||||
BABY_LOVE_DRAWING_EDUTAINMENT_TAG,
|
||||
BABY_LOVE_DRAWING_TEMPLATE_NAME,
|
||||
]),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
saveRecordToLocalStore(record);
|
||||
return { record };
|
||||
}
|
||||
|
||||
export async function createBabyLoveDrawingMagicImage(
|
||||
payload: CreateBabyLoveDrawingMagicRequest,
|
||||
) {
|
||||
return requestJson<CreateBabyLoveDrawingMagicResponse>(
|
||||
BABY_LOVE_DRAWING_MAGIC_API,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'生成宝贝爱画魔法图片失败',
|
||||
{
|
||||
retry: BABY_LOVE_DRAWING_MAGIC_RETRY,
|
||||
timeoutMs: 180000,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export const babyDrawingClient = {
|
||||
createMagicImage: createBabyLoveDrawingMagicImage,
|
||||
listLocalDrawings: listLocalBabyLoveDrawings,
|
||||
saveDrawing: saveBabyLoveDrawing,
|
||||
};
|
||||
1
src/services/edutainment-baby-drawing/index.ts
Normal file
1
src/services/edutainment-baby-drawing/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './babyDrawingClient';
|
||||
Reference in New Issue
Block a user