1
This commit is contained in:
@@ -629,6 +629,104 @@ export async function sendCustomWorldAgentMessage(
|
||||
);
|
||||
}
|
||||
|
||||
export async function streamCustomWorldAgentMessage(
|
||||
sessionId: string,
|
||||
payload: SendCustomWorldAgentMessageRequest,
|
||||
options: TextStreamOptions = {},
|
||||
) {
|
||||
const response = await fetchWithApiAuth(
|
||||
`${RUNTIME_API_BASE}/custom-world/agent/sessions/${encodeURIComponent(sessionId)}/messages/stream`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const responseText = await response.text();
|
||||
throw new Error(parseApiErrorMessage(responseText, '发送共创消息失败'));
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error('streaming response body is unavailable');
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
let buffer = '';
|
||||
let finalSession: CustomWorldAgentSessionSnapshot | null = null;
|
||||
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
while (buffer.includes('\n\n')) {
|
||||
const boundary = buffer.indexOf('\n\n');
|
||||
const eventBlock = buffer.slice(0, boundary);
|
||||
buffer = buffer.slice(boundary + 2);
|
||||
|
||||
let eventName = 'message';
|
||||
const dataLines: string[] = [];
|
||||
|
||||
for (const rawLine of eventBlock.split(/\r?\n/u)) {
|
||||
const line = rawLine.trim();
|
||||
if (line.startsWith('event:')) {
|
||||
eventName = line.slice(6).trim() || 'message';
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith('data:')) {
|
||||
dataLines.push(line.slice(5).trim());
|
||||
}
|
||||
}
|
||||
|
||||
if (dataLines.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = dataLines.join('\n');
|
||||
let parsed: Record<string, unknown> | null = null;
|
||||
|
||||
try {
|
||||
parsed = JSON.parse(data) as Record<string, unknown>;
|
||||
} catch {
|
||||
parsed = null;
|
||||
}
|
||||
|
||||
if (eventName === 'reply_delta' && parsed) {
|
||||
const text = parsed.text;
|
||||
if (typeof text === 'string') {
|
||||
options.onUpdate?.(text);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (eventName === 'session' && parsed?.session) {
|
||||
finalSession = parsed.session as CustomWorldAgentSessionSnapshot;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (eventName === 'error' && parsed) {
|
||||
const message =
|
||||
typeof parsed.message === 'string' && parsed.message.trim()
|
||||
? parsed.message.trim()
|
||||
: '发送共创消息失败';
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!finalSession) {
|
||||
throw new Error('共创消息流式结果不完整');
|
||||
}
|
||||
|
||||
return finalSession;
|
||||
}
|
||||
|
||||
export async function executeCustomWorldAgentAction(
|
||||
sessionId: string,
|
||||
payload: CustomWorldAgentActionRequest,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { EightAnchorContent } from '../../packages/shared/src/contracts/customWorldAgent';
|
||||
import {
|
||||
AFFINITY_BACKSTORY_CHAPTER_THRESHOLDS,
|
||||
DEFAULT_PRIVATE_CHAT_UNLOCK_AFFINITY,
|
||||
@@ -93,6 +94,9 @@ export interface CustomWorldGenerationRoleOutline {
|
||||
title: string;
|
||||
role: string;
|
||||
description: string;
|
||||
visualDescription?: string;
|
||||
actionDescription?: string;
|
||||
sceneVisualDescription?: string;
|
||||
initialAffinity: number;
|
||||
relationshipHooks: string[];
|
||||
tags: string[];
|
||||
@@ -107,6 +111,7 @@ export interface CustomWorldGenerationLandmarkConnectionOutline {
|
||||
export interface CustomWorldGenerationLandmarkOutline {
|
||||
name: string;
|
||||
description: string;
|
||||
visualDescription?: string;
|
||||
dangerLevel: string;
|
||||
sceneNpcNames: string[];
|
||||
connections: CustomWorldGenerationLandmarkConnectionOutline[];
|
||||
@@ -148,6 +153,12 @@ function toFiniteInteger(value: unknown) {
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function toFiniteNumber(value: unknown) {
|
||||
return typeof value === 'number' && Number.isFinite(value)
|
||||
? value
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function toRecordArray(value: unknown) {
|
||||
return Array.isArray(value)
|
||||
? (value.filter((item) => item && typeof item === 'object') as Array<
|
||||
@@ -198,6 +209,11 @@ function normalizeGeneratedAnimationConfig(
|
||||
const extension = toText(item.extension);
|
||||
const file = toText(item.file);
|
||||
const basePath = toText(item.basePath);
|
||||
const frameWidth = toFiniteInteger(item.frameWidth);
|
||||
const frameHeight = toFiniteInteger(item.frameHeight);
|
||||
const fps = toFiniteNumber(item.fps);
|
||||
const loop = typeof item.loop === 'boolean' ? item.loop : undefined;
|
||||
const previewVideoPath = toText(item.previewVideoPath);
|
||||
|
||||
return {
|
||||
folder,
|
||||
@@ -207,6 +223,11 @@ function normalizeGeneratedAnimationConfig(
|
||||
...(extension ? { extension } : {}),
|
||||
...(file ? { file } : {}),
|
||||
...(basePath ? { basePath } : {}),
|
||||
...(frameWidth ? { frameWidth: Math.max(1, frameWidth) } : {}),
|
||||
...(frameHeight ? { frameHeight: Math.max(1, frameHeight) } : {}),
|
||||
...(fps ? { fps: Math.max(1, fps) } : {}),
|
||||
...(typeof loop === 'boolean' ? { loop } : {}),
|
||||
...(previewVideoPath ? { previewVideoPath } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -762,6 +783,9 @@ export function buildCustomWorldRawProfileFromFramework(
|
||||
title: npc.title,
|
||||
role: npc.role,
|
||||
description: npc.description,
|
||||
visualDescription: npc.visualDescription,
|
||||
actionDescription: npc.actionDescription,
|
||||
sceneVisualDescription: npc.sceneVisualDescription,
|
||||
initialAffinity: npc.initialAffinity,
|
||||
relationshipHooks: [...npc.relationshipHooks],
|
||||
tags: [...npc.tags],
|
||||
@@ -771,6 +795,9 @@ export function buildCustomWorldRawProfileFromFramework(
|
||||
title: npc.title,
|
||||
role: npc.role,
|
||||
description: npc.description,
|
||||
visualDescription: npc.visualDescription,
|
||||
actionDescription: npc.actionDescription,
|
||||
sceneVisualDescription: npc.sceneVisualDescription,
|
||||
initialAffinity: npc.initialAffinity,
|
||||
relationshipHooks: [...npc.relationshipHooks],
|
||||
tags: [...npc.tags],
|
||||
@@ -778,6 +805,7 @@ export function buildCustomWorldRawProfileFromFramework(
|
||||
landmarks: framework.landmarks.map((landmark) => ({
|
||||
name: landmark.name,
|
||||
description: landmark.description,
|
||||
visualDescription: landmark.visualDescription,
|
||||
dangerLevel: landmark.dangerLevel,
|
||||
sceneNpcNames: [...landmark.sceneNpcNames],
|
||||
connections: landmark.connections.map((connection) => ({
|
||||
@@ -812,6 +840,9 @@ function normalizeRoleProfile(
|
||||
title,
|
||||
role,
|
||||
description: toText(item.description),
|
||||
visualDescription: toText(item.visualDescription),
|
||||
actionDescription: toText(item.actionDescription),
|
||||
sceneVisualDescription: toText(item.sceneVisualDescription),
|
||||
backstory: toText(item.backstory),
|
||||
personality: toText(item.personality),
|
||||
motivation: toText(item.motivation) || toText(item.description),
|
||||
@@ -923,6 +954,9 @@ function normalizeRoleOutlineList(
|
||||
description:
|
||||
toText(item.description) ||
|
||||
truncateText(`${name || title}在世界中以${role}身份活动。`, 36),
|
||||
visualDescription: toText(item.visualDescription) || undefined,
|
||||
actionDescription: toText(item.actionDescription) || undefined,
|
||||
sceneVisualDescription: toText(item.sceneVisualDescription) || undefined,
|
||||
initialAffinity: normalizeInitialAffinity(
|
||||
item.initialAffinity,
|
||||
options.defaultAffinity,
|
||||
@@ -973,6 +1007,7 @@ function normalizeLandmarkOutlineList(value: unknown) {
|
||||
description:
|
||||
toText(item.description) ||
|
||||
truncateText(`${name}暗藏新的局势变化。`, 40),
|
||||
visualDescription: toText(item.visualDescription) || undefined,
|
||||
dangerLevel: toText(item.dangerLevel) || 'medium',
|
||||
sceneNpcNames: [
|
||||
...toStringArray(item.sceneNpcNames),
|
||||
@@ -1033,6 +1068,7 @@ function normalizeLandmarkDraftList(value: unknown) {
|
||||
id: toText(item.id) || createEntryId('landmark', name, index),
|
||||
name,
|
||||
description: toText(item.description),
|
||||
visualDescription: toText(item.visualDescription) || undefined,
|
||||
dangerLevel: toText(item.dangerLevel),
|
||||
imageSrc: toText(item.imageSrc) || undefined,
|
||||
sceneNpcIds: toStringArray(item.sceneNpcIds),
|
||||
@@ -1165,6 +1201,10 @@ export function normalizeCustomWorldProfile(
|
||||
item.storyGraph && typeof item.storyGraph === 'object'
|
||||
? (item.storyGraph as WorldStoryGraph)
|
||||
: null,
|
||||
anchorContent:
|
||||
item.anchorContent && typeof item.anchorContent === 'object'
|
||||
? (item.anchorContent as EightAnchorContent)
|
||||
: null,
|
||||
creatorIntent: normalizeCustomWorldCreatorIntent(item.creatorIntent),
|
||||
anchorPack:
|
||||
item.anchorPack && typeof item.anchorPack === 'object'
|
||||
|
||||
@@ -13,6 +13,31 @@ afterEach(() => {
|
||||
|
||||
const session: CustomWorldAgentSessionSnapshot = {
|
||||
sessionId: 'session-1',
|
||||
currentTurn: 6,
|
||||
anchorContent: {
|
||||
worldPromise: {
|
||||
hook: '被海雾吞没的旧航路群岛',
|
||||
differentiator: '灯塔与禁航令共同决定谁能活着穿过去。',
|
||||
desiredExperience: '压抑、悬疑、潮湿',
|
||||
},
|
||||
playerFantasy: {
|
||||
playerRole: '玩家回到群岛调查沉船真相。',
|
||||
corePursuit: '找出失控航路背后的真相。',
|
||||
fearOfLoss: '失去最后一个还能对上旧案的人。',
|
||||
},
|
||||
themeBoundary: null,
|
||||
playerEntryPoint: null,
|
||||
coreConflict: null,
|
||||
keyRelationships: [],
|
||||
hiddenLines: null,
|
||||
iconicElements: {
|
||||
iconicMotifs: ['会移动的海雾'],
|
||||
institutionsOrArtifacts: ['旧灯塔'],
|
||||
hardRules: [],
|
||||
},
|
||||
},
|
||||
progressPercent: 100,
|
||||
lastAssistantReply: '八锚点已经齐备,可以进入游戏设定草稿生成。',
|
||||
stage: 'object_refining',
|
||||
focusCardId: null,
|
||||
creatorIntent: {
|
||||
|
||||
@@ -231,6 +231,7 @@ export function buildCustomWorldProfileFromAgentDraft(
|
||||
imageSrc: toText(draftProfile.camp.imageSrc) || undefined,
|
||||
}
|
||||
: undefined,
|
||||
anchorContent: session.anchorContent,
|
||||
creatorIntent: session.creatorIntent,
|
||||
anchorPack: session.anchorPack,
|
||||
lockState: session.lockState,
|
||||
|
||||
@@ -22,6 +22,31 @@ const baseOperation: CustomWorldAgentOperationRecord = {
|
||||
|
||||
const baseSession: CustomWorldAgentSessionSnapshot = {
|
||||
sessionId: 'session-1',
|
||||
currentTurn: 8,
|
||||
anchorContent: {
|
||||
worldPromise: {
|
||||
hook: '海雾、旧灯塔和失控航路交织的边缘群岛',
|
||||
differentiator: '每次借路都要向海雾付出新的代价。',
|
||||
desiredExperience: '压抑、悬疑、潮湿',
|
||||
},
|
||||
playerFantasy: {
|
||||
playerRole: '玩家刚回到群岛,准备调查父亲沉船的真相。',
|
||||
corePursuit: '查清沉船夜和禁航区异动的因果。',
|
||||
fearOfLoss: '再失去唯一还敢接近真相的人。',
|
||||
},
|
||||
themeBoundary: null,
|
||||
playerEntryPoint: null,
|
||||
coreConflict: null,
|
||||
keyRelationships: [],
|
||||
hiddenLines: null,
|
||||
iconicElements: {
|
||||
iconicMotifs: ['会移动的海雾'],
|
||||
institutionsOrArtifacts: ['旧灯塔'],
|
||||
hardRules: [],
|
||||
},
|
||||
},
|
||||
progressPercent: 100,
|
||||
lastAssistantReply: '八锚点已经收束完成,可以进入游戏设定草稿生成。',
|
||||
stage: 'foundation_review',
|
||||
focusCardId: null,
|
||||
creatorIntent: {
|
||||
@@ -121,11 +146,12 @@ test('builds readable draft setting text from creator intent first', () => {
|
||||
expect(settingText).toContain('标志元素');
|
||||
});
|
||||
|
||||
test('falls back to latest user message when creator intent is unavailable', () => {
|
||||
test('falls back to anchor content when creator intent is unavailable', () => {
|
||||
const settingText = buildAgentDraftFoundationSettingText({
|
||||
...baseSession,
|
||||
creatorIntent: null,
|
||||
});
|
||||
|
||||
expect(settingText).toBe('我想做一个被海雾吞没的旧航路世界。');
|
||||
expect(settingText).toContain('世界承诺');
|
||||
expect(settingText).toContain('玩家幻想');
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
CustomWorldAgentOperationRecord,
|
||||
CustomWorldAgentSessionSnapshot,
|
||||
EightAnchorContent,
|
||||
} from '../../packages/shared/src/contracts/customWorldAgent';
|
||||
import type {
|
||||
CustomWorldGenerationProgress,
|
||||
@@ -11,6 +12,187 @@ import {
|
||||
normalizeCustomWorldCreatorIntent,
|
||||
} from './customWorldCreatorIntent';
|
||||
|
||||
export type CustomWorldStructuredAnchorEntry = {
|
||||
id: string;
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
function joinText(items: Array<string | null | undefined>) {
|
||||
return items.filter(Boolean).join(';');
|
||||
}
|
||||
|
||||
function buildEightAnchorFoundationText(anchorContent: EightAnchorContent) {
|
||||
return [
|
||||
anchorContent.worldPromise
|
||||
? `世界承诺:${joinText([
|
||||
anchorContent.worldPromise.hook,
|
||||
anchorContent.worldPromise.differentiator,
|
||||
anchorContent.worldPromise.desiredExperience,
|
||||
])}`
|
||||
: '',
|
||||
anchorContent.playerFantasy
|
||||
? `玩家幻想:${joinText([
|
||||
anchorContent.playerFantasy.playerRole,
|
||||
anchorContent.playerFantasy.corePursuit,
|
||||
anchorContent.playerFantasy.fearOfLoss,
|
||||
])}`
|
||||
: '',
|
||||
anchorContent.themeBoundary
|
||||
? `主题边界:${joinText([
|
||||
anchorContent.themeBoundary.toneKeywords.join('、'),
|
||||
anchorContent.themeBoundary.aestheticDirectives.join('、'),
|
||||
anchorContent.themeBoundary.forbiddenDirectives.join('、'),
|
||||
])}`
|
||||
: '',
|
||||
anchorContent.playerEntryPoint
|
||||
? `玩家切入口:${joinText([
|
||||
anchorContent.playerEntryPoint.openingIdentity,
|
||||
anchorContent.playerEntryPoint.openingProblem,
|
||||
anchorContent.playerEntryPoint.entryMotivation,
|
||||
])}`
|
||||
: '',
|
||||
anchorContent.coreConflict
|
||||
? `核心冲突:${joinText([
|
||||
anchorContent.coreConflict.surfaceConflicts.join('、'),
|
||||
anchorContent.coreConflict.hiddenCrisis,
|
||||
anchorContent.coreConflict.firstTouchedConflict,
|
||||
])}`
|
||||
: '',
|
||||
anchorContent.keyRelationships.length > 0
|
||||
? `关键关系:${anchorContent.keyRelationships
|
||||
.map((entry) =>
|
||||
joinText([entry.pairs, entry.relationshipType, entry.secretOrCost]),
|
||||
)
|
||||
.join(';')}`
|
||||
: '',
|
||||
anchorContent.hiddenLines
|
||||
? `暗线与揭示:${joinText([
|
||||
anchorContent.hiddenLines.hiddenTruths.join('、'),
|
||||
anchorContent.hiddenLines.misdirectionHints.join('、'),
|
||||
anchorContent.hiddenLines.revealPacing,
|
||||
])}`
|
||||
: '',
|
||||
anchorContent.iconicElements
|
||||
? `标志元素:${joinText([
|
||||
anchorContent.iconicElements.iconicMotifs.join('、'),
|
||||
anchorContent.iconicElements.institutionsOrArtifacts.join('、'),
|
||||
anchorContent.iconicElements.hardRules.join('、'),
|
||||
])}`
|
||||
: '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
export function buildAgentDraftFoundationAnchorEntries(
|
||||
session: CustomWorldAgentSessionSnapshot | null | undefined,
|
||||
): CustomWorldStructuredAnchorEntry[] {
|
||||
if (!session) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const anchorContent = session.anchorContent;
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'world-promise',
|
||||
label: '世界承诺',
|
||||
value: anchorContent.worldPromise
|
||||
? joinText([
|
||||
anchorContent.worldPromise.hook,
|
||||
anchorContent.worldPromise.differentiator,
|
||||
anchorContent.worldPromise.desiredExperience,
|
||||
])
|
||||
: '',
|
||||
},
|
||||
{
|
||||
id: 'player-fantasy',
|
||||
label: '玩家幻想',
|
||||
value: anchorContent.playerFantasy
|
||||
? joinText([
|
||||
anchorContent.playerFantasy.playerRole,
|
||||
anchorContent.playerFantasy.corePursuit,
|
||||
anchorContent.playerFantasy.fearOfLoss,
|
||||
])
|
||||
: '',
|
||||
},
|
||||
{
|
||||
id: 'theme-boundary',
|
||||
label: '主题边界',
|
||||
value: anchorContent.themeBoundary
|
||||
? joinText([
|
||||
anchorContent.themeBoundary.toneKeywords.join('、'),
|
||||
anchorContent.themeBoundary.aestheticDirectives.join('、'),
|
||||
anchorContent.themeBoundary.forbiddenDirectives.length > 0
|
||||
? `避免:${anchorContent.themeBoundary.forbiddenDirectives.join('、')}`
|
||||
: '',
|
||||
])
|
||||
: '',
|
||||
},
|
||||
{
|
||||
id: 'player-entry-point',
|
||||
label: '玩家切入口',
|
||||
value: anchorContent.playerEntryPoint
|
||||
? joinText([
|
||||
anchorContent.playerEntryPoint.openingIdentity,
|
||||
anchorContent.playerEntryPoint.openingProblem,
|
||||
anchorContent.playerEntryPoint.entryMotivation,
|
||||
])
|
||||
: '',
|
||||
},
|
||||
{
|
||||
id: 'core-conflict',
|
||||
label: '核心冲突',
|
||||
value: anchorContent.coreConflict
|
||||
? joinText([
|
||||
anchorContent.coreConflict.surfaceConflicts.join('、'),
|
||||
anchorContent.coreConflict.hiddenCrisis,
|
||||
anchorContent.coreConflict.firstTouchedConflict,
|
||||
])
|
||||
: '',
|
||||
},
|
||||
{
|
||||
id: 'key-relationships',
|
||||
label: '关键关系',
|
||||
value:
|
||||
anchorContent.keyRelationships.length > 0
|
||||
? anchorContent.keyRelationships
|
||||
.map((entry) =>
|
||||
joinText([
|
||||
entry.pairs,
|
||||
entry.relationshipType,
|
||||
entry.secretOrCost ? `代价/秘密:${entry.secretOrCost}` : '',
|
||||
]),
|
||||
)
|
||||
.join('\n')
|
||||
: '',
|
||||
},
|
||||
{
|
||||
id: 'hidden-lines',
|
||||
label: '暗线与揭示',
|
||||
value: anchorContent.hiddenLines
|
||||
? joinText([
|
||||
anchorContent.hiddenLines.hiddenTruths.join('、'),
|
||||
anchorContent.hiddenLines.misdirectionHints.join('、'),
|
||||
anchorContent.hiddenLines.revealPacing,
|
||||
])
|
||||
: '',
|
||||
},
|
||||
{
|
||||
id: 'iconic-elements',
|
||||
label: '标志元素',
|
||||
value: anchorContent.iconicElements
|
||||
? joinText([
|
||||
anchorContent.iconicElements.iconicMotifs.join('、'),
|
||||
anchorContent.iconicElements.institutionsOrArtifacts.join('、'),
|
||||
anchorContent.iconicElements.hardRules.join('、'),
|
||||
])
|
||||
: '',
|
||||
},
|
||||
].filter((entry) => entry.value.trim());
|
||||
}
|
||||
|
||||
const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [
|
||||
{
|
||||
id: 'queue',
|
||||
@@ -192,5 +374,11 @@ export function buildAgentDraftFoundationSettingText(
|
||||
.reverse()
|
||||
.find((message) => message.role === 'user' && message.text.trim());
|
||||
|
||||
return latestUserMessage?.text.trim() ?? '正在整理当前共创设定。';
|
||||
const anchorSettingText = buildEightAnchorFoundationText(session.anchorContent);
|
||||
|
||||
return (
|
||||
anchorSettingText ||
|
||||
latestUserMessage?.text.trim() ||
|
||||
'正在整理当前共创设定。'
|
||||
);
|
||||
}
|
||||
|
||||
105
src/services/storageService.test.ts
Normal file
105
src/services/storageService.test.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const { requestJsonMock } = vi.hoisted(() => ({
|
||||
requestJsonMock: vi.fn(),
|
||||
}));
|
||||
|
||||
import {
|
||||
clearProfileBrowseHistory,
|
||||
listProfileBrowseHistory,
|
||||
syncProfileBrowseHistory,
|
||||
upsertProfileBrowseHistory,
|
||||
} from './storageService';
|
||||
|
||||
vi.mock('./apiClient', () => ({
|
||||
requestJson: requestJsonMock,
|
||||
}));
|
||||
|
||||
describe('storageService browse history routes', () => {
|
||||
beforeEach(() => {
|
||||
requestJsonMock.mockReset();
|
||||
requestJsonMock.mockResolvedValue({ entries: [] });
|
||||
});
|
||||
|
||||
it('reads browse history from the runtime profile route', async () => {
|
||||
await listProfileBrowseHistory();
|
||||
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
'/api/runtime/profile/browse-history',
|
||||
expect.objectContaining({ method: 'GET' }),
|
||||
'读取浏览历史失败',
|
||||
expect.objectContaining({
|
||||
retry: expect.objectContaining({ maxRetries: 1 }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('writes browse history through the runtime profile route', async () => {
|
||||
await upsertProfileBrowseHistory({
|
||||
ownerUserId: 'user-1',
|
||||
profileId: 'profile-1',
|
||||
worldName: '测试世界',
|
||||
subtitle: '测试副标题',
|
||||
summaryText: '测试摘要',
|
||||
coverImageSrc: null,
|
||||
themeMode: 'mythic',
|
||||
authorDisplayName: '测试作者',
|
||||
});
|
||||
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
'/api/runtime/profile/browse-history',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
'写入浏览历史失败',
|
||||
expect.objectContaining({
|
||||
retry: expect.objectContaining({
|
||||
maxRetries: 1,
|
||||
retryUnsafeMethods: true,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('syncs browse history through the runtime profile route', async () => {
|
||||
await syncProfileBrowseHistory([
|
||||
{
|
||||
ownerUserId: 'user-1',
|
||||
profileId: 'profile-1',
|
||||
worldName: '测试世界',
|
||||
subtitle: '测试副标题',
|
||||
summaryText: '测试摘要',
|
||||
coverImageSrc: null,
|
||||
themeMode: 'mythic',
|
||||
authorDisplayName: '测试作者',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
'/api/runtime/profile/browse-history',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
'同步浏览历史失败',
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it('clears browse history through the runtime profile route', async () => {
|
||||
await clearProfileBrowseHistory();
|
||||
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
'/api/runtime/profile/browse-history',
|
||||
expect.objectContaining({ method: 'DELETE' }),
|
||||
'清空浏览历史失败',
|
||||
expect.objectContaining({
|
||||
retry: expect.objectContaining({
|
||||
maxRetries: 1,
|
||||
retryUnsafeMethods: true,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -61,28 +61,6 @@ function requestRuntimeJson<T>(
|
||||
);
|
||||
}
|
||||
|
||||
function requestProfileJson<T>(
|
||||
path: string,
|
||||
init: RequestInit,
|
||||
fallbackMessage: string,
|
||||
options: RuntimeRequestOptions = {},
|
||||
) {
|
||||
const method = (init.method ?? 'GET').toUpperCase();
|
||||
const retry =
|
||||
options.retry ??
|
||||
(method === 'GET' ? RUNTIME_READ_RETRY : RUNTIME_WRITE_RETRY);
|
||||
|
||||
return requestJson<T>(
|
||||
`/api/profile${path}`,
|
||||
{
|
||||
...init,
|
||||
signal: options.signal,
|
||||
},
|
||||
fallbackMessage,
|
||||
{ retry },
|
||||
);
|
||||
}
|
||||
|
||||
export async function getSaveSnapshot(options: RuntimeRequestOptions = {}) {
|
||||
const snapshot = await requestRuntimeJson<HydratedSavedGameSnapshot | null>(
|
||||
'/save/snapshot',
|
||||
@@ -315,8 +293,8 @@ export async function getCustomWorldGalleryDetail(
|
||||
export async function listProfileBrowseHistory(
|
||||
options: RuntimeRequestOptions = {},
|
||||
) {
|
||||
const response = await requestProfileJson<PlatformBrowseHistoryResponse>(
|
||||
'/browse-history',
|
||||
const response = await requestRuntimeJson<PlatformBrowseHistoryResponse>(
|
||||
'/profile/browse-history',
|
||||
{ method: 'GET' },
|
||||
'读取浏览历史失败',
|
||||
options,
|
||||
@@ -329,8 +307,8 @@ export async function upsertProfileBrowseHistory(
|
||||
entry: PlatformBrowseHistoryWriteEntry,
|
||||
options: RuntimeRequestOptions = {},
|
||||
) {
|
||||
const response = await requestProfileJson<PlatformBrowseHistoryResponse>(
|
||||
'/browse-history',
|
||||
const response = await requestRuntimeJson<PlatformBrowseHistoryResponse>(
|
||||
'/profile/browse-history',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -347,8 +325,8 @@ export async function syncProfileBrowseHistory(
|
||||
entries: PlatformBrowseHistoryWriteEntry[],
|
||||
options: RuntimeRequestOptions = {},
|
||||
) {
|
||||
const response = await requestProfileJson<PlatformBrowseHistoryResponse>(
|
||||
'/browse-history',
|
||||
const response = await requestRuntimeJson<PlatformBrowseHistoryResponse>(
|
||||
'/profile/browse-history',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -366,8 +344,8 @@ export async function syncProfileBrowseHistory(
|
||||
export async function clearProfileBrowseHistory(
|
||||
options: RuntimeRequestOptions = {},
|
||||
) {
|
||||
const response = await requestProfileJson<PlatformBrowseHistoryResponse>(
|
||||
'/browse-history',
|
||||
const response = await requestRuntimeJson<PlatformBrowseHistoryResponse>(
|
||||
'/profile/browse-history',
|
||||
{ method: 'DELETE' },
|
||||
'清空浏览历史失败',
|
||||
options,
|
||||
|
||||
Reference in New Issue
Block a user