Close DDD refactor and remove generated asset proxy

This commit is contained in:
kdletters
2026-05-02 00:27:22 +08:00
parent fd08262bf0
commit 9d9913095d
605 changed files with 11811 additions and 10106 deletions

View File

@@ -10,6 +10,7 @@ import type {
StoryOption,
} from '../types';
import { AnimationState, WorldType } from '../types';
import type { StoryRuntimeProjectionResponse } from '../../packages/shared/src/contracts/story';
const {
connectivityError,
@@ -101,6 +102,7 @@ function createContext(
): StoryGenerationContext {
return {
runtimeSessionId: 'runtime-main',
storySessionId: 'storysess-main',
runtimeActionVersion: 3,
playerHp: 30,
playerMaxHp: 40,
@@ -115,6 +117,8 @@ function createContext(
sceneName: 'Forest Trail',
sceneDescription: 'A quiet mountain path.',
pendingSceneEncounter: false,
observeSignsRequested: false,
recentActionResult: null,
...overrides,
};
}
@@ -424,6 +428,71 @@ function createApiEnvelopeResponse(data: unknown) {
} as Response;
}
type RuntimeProjectionOverrides = Omit<
Partial<StoryRuntimeProjectionResponse>,
'storySession'
> & {
storySession?: Partial<StoryRuntimeProjectionResponse['storySession']>;
};
function createRuntimeProjection(
overrides: RuntimeProjectionOverrides = {},
): StoryRuntimeProjectionResponse {
const storySession = {
storySessionId: 'storysess-main',
runtimeSessionId: 'runtime-main',
actorUserId: 'user-main',
worldProfileId: 'profile-main',
initialPrompt: '进入山路',
openingSummary: null,
latestNarrativeText: '山路尽头传来新的动静。',
latestChoiceFunctionId: null,
status: 'active',
version: 3,
createdAt: '2026-04-08T00:00:00.000Z',
updatedAt: '2026-04-08T00:00:01.000Z',
...(overrides.storySession ?? {}),
} satisfies StoryRuntimeProjectionResponse['storySession'];
return {
storySession,
storyEvents: overrides.storyEvents ?? [],
serverVersion: overrides.serverVersion ?? storySession.version,
gameState: {
runtimeSessionId: storySession.runtimeSessionId,
storySessionId: storySession.storySessionId,
runtimeActionVersion: overrides.serverVersion ?? storySession.version,
currentScene: 'Story',
playerEquipment: { weapon: null, armor: null, relic: null },
...(overrides.gameState ?? {}),
},
actor: overrides.actor ?? {
hp: 30,
maxHp: 40,
mana: 12,
maxMana: 20,
currency: 0,
currencyText: '0 铜钱',
},
inventory: overrides.inventory ?? {
backpackItems: [],
equipmentSlots: [],
forgeRecipes: [],
},
options: overrides.options ?? [],
status: overrides.status ?? {
inBattle: false,
npcInteractionActive: false,
currentNpcBattleMode: null,
currentNpcBattleOutcome: null,
},
currentNarrativeText:
overrides.currentNarrativeText ?? storySession.latestNarrativeText,
actionResultText: overrides.actionResultText ?? null,
toast: overrides.toast ?? null,
};
}
function createSseResponse(text: string) {
const encoder = new TextEncoder();
const chunks = [
@@ -478,14 +547,26 @@ describe('ai runtime client orchestration', () => {
streamPlainTextCompletionMock.mockReset();
});
it('requests initial story from the runtime api server', async () => {
it('requests initial story from the story session projection', async () => {
const availableOptions = [createStoryOption()];
fetchMock.mockResolvedValue(
createApiEnvelopeResponse({
storyText: '山路尽头传来新的动静。',
options: availableOptions,
encounter: null,
}),
createApiEnvelopeResponse(
createRuntimeProjection({
options: availableOptions.map((option) => ({
functionId: option.functionId,
actionText: option.actionText,
detailText: null,
scope: 'story',
payload: null,
enabled: true,
reason: null,
})),
currentNarrativeText: '山路尽头传来新的动静。',
storySession: {
latestNarrativeText: '山路尽头传来新的动静。',
},
}),
),
);
const response = await generateInitialStory(
@@ -497,21 +578,16 @@ describe('ai runtime client orchestration', () => {
);
expect(fetchMock).toHaveBeenCalledWith(
'/api/runtime/story/initial',
'/api/story/sessions/storysess-main/runtime-projection',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
sessionId: 'runtime-main',
clientVersion: 3,
requestOptions: { availableOptions },
}),
method: 'GET',
}),
);
expect(response.storyText).toBe('山路尽头传来新的动静。');
expect(response.options).toEqual(availableOptions);
});
it('requests next story step from the runtime api server', async () => {
it('requests next story step from the story session action endpoint', async () => {
const availableOptions = [
createStoryOption({
functionId: 'idle_explore_forward',
@@ -521,9 +597,24 @@ describe('ai runtime client orchestration', () => {
];
fetchMock.mockResolvedValue(
createApiEnvelopeResponse({
storyText: '林间重新安静下来,你听见远处的风声。',
encounter: null,
options: availableOptions,
projection: createRuntimeProjection({
serverVersion: 4,
currentNarrativeText: '林间重新安静下来,你听见远处的风声。',
storySession: {
latestNarrativeText: '林间重新安静下来,你听见远处的风声。',
latestChoiceFunctionId: 'idle_explore_forward',
version: 4,
},
options: availableOptions.map((option) => ({
functionId: option.functionId,
actionText: option.actionText,
detailText: null,
scope: 'story',
payload: null,
enabled: true,
reason: null,
})),
}),
}),
);
@@ -533,19 +624,27 @@ describe('ai runtime client orchestration', () => {
monsters,
storyHistory,
'继续向前',
context,
{
...context,
lastFunctionId: 'idle_explore_forward',
},
{ availableOptions },
);
expect(fetchMock).toHaveBeenCalledWith(
'/api/runtime/story/continue',
'/api/story/sessions/storysess-main/actions/resolve',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
sessionId: 'runtime-main',
storySessionId: 'storysess-main',
clientVersion: 3,
choice: '继续向前',
requestOptions: { availableOptions },
functionId: 'idle_explore_forward',
actionText: '继续向前',
payload: {
optionText: '继续向前',
observeSignsRequested: false,
recentActionResult: null,
},
}),
}),
);
@@ -972,11 +1071,11 @@ describe('ai runtime client orchestration', () => {
});
const sceneImageCalls = fetchMock.mock.calls.filter(
([url]) => url === '/api/custom-world/scene-image',
([url]) => url === '/api/runtime/custom-world/scene-image',
);
expect(sceneImageCalls).toHaveLength(1);
expect(sceneImageCalls[0]).toEqual([
'/api/custom-world/scene-image',
'/api/runtime/custom-world/scene-image',
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({