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

@@ -8,15 +8,16 @@ import {
TASK5_RUNTIME_FUNCTION_IDS,
} from '../../../packages/shared/src/contracts/rpgRuntimeStoryAction';
import type {
RuntimeStoryActionRequest,
RuntimeStoryActionResponse,
RuntimeStoryBootstrapRequest,
RuntimeStoryBootstrapResponse,
RuntimeStoryOptionView,
RuntimeBattlePresentation,
RuntimeStoryInventoryViewModel,
} from '../../../packages/shared/src/contracts/rpgRuntimeStoryState';
import type {
BeginStoryRuntimeSessionRequest,
BeginStorySessionRequest,
ContinueStoryRequest,
ResolveStoryRuntimeActionRequest,
StoryRuntimeMutationResponse,
StorySessionMutationResponse,
StorySessionStateResponse,
StoryRuntimeOptionProjection,
@@ -28,7 +29,6 @@ import type { GameState, StoryMoment, StoryOption } from '../../types';
import { AnimationState } from '../../types';
import { type ApiRetryOptions, requestJson } from '../apiClient';
const RUNTIME_STORY_API_BASE = '/api/runtime/story';
const STORY_SESSIONS_API_BASE = '/api/story/sessions';
const DEFAULT_SESSION_ID = 'runtime-main';
const RUNTIME_STORY_RETRY: ApiRetryOptions = {
@@ -50,40 +50,25 @@ export type RpgRuntimeStoryClientOptions = {
retry?: ApiRetryOptions;
};
export type RuntimeStoryResponse = RuntimeStoryActionResponse<
GameState,
StoryMoment
>;
export type RuntimeStoryBootstrapResult = RuntimeStoryBootstrapResponse<
GameState,
StoryMoment
>;
export type RuntimeStoryActionPresentation = {
battle?: RuntimeBattlePresentation | null;
resultText?: string;
storyText?: string;
};
export type RuntimeStoryInventoryView = RuntimeStoryInventoryViewModel;
export type RuntimeStoryResponse = {
sessionId: string;
serverVersion: number;
projection: StoryRuntimeProjectionResponse;
snapshot: HydratedSavedGameSnapshot;
inventoryView: RuntimeStoryInventoryView;
presentation?: RuntimeStoryActionPresentation;
};
export type StorySessionMutationResult = StorySessionMutationResponse;
export type StorySessionStateResult = StorySessionStateResponse;
export type RuntimeStoryProjectionResult = StoryRuntimeProjectionResponse;
export type RuntimeStoryInventoryView =
RuntimeStoryResponse['viewModel']['inventory'];
export type { RuntimeStoryChoicePayload };
function requestRuntimeStoryJson<T>(
path: string,
init: RequestInit,
fallbackMessage: string,
options: RpgRuntimeStoryClientOptions = {},
) {
// 中文注释runtime story 请求默认带一层轻量重试,
// 因为这里既有 state 拉取,也有动作结算,请求失败会直接影响当前回合体验。
return requestJson<T>(
`${RUNTIME_STORY_API_BASE}${path}`,
{
...init,
signal: options.signal,
},
fallbackMessage,
{ retry: options.retry ?? RUNTIME_STORY_RETRY },
);
}
function requestStorySessionJson<T>(
path: string,
init: RequestInit,
@@ -105,7 +90,7 @@ function createRuntimeStoryOption(
option: RuntimeStoryOptionView,
_gameState?: Pick<GameState, 'currentEncounter'>,
): StoryOption {
// 中文注释:服务端 viewModel 当前只返回动作层字段,
// 中文注释:服务端投影当前只返回动作层字段,
// 前端在这里补齐 StoryOption 所需的基础表现字段,保持冒险面板消费接口稳定。
return {
functionId: option.functionId,
@@ -178,6 +163,47 @@ function getRuntimeProjectionStoryText(
);
}
export function buildRuntimeSnapshotFromProjection(
projection: StoryRuntimeProjectionResponse,
bottomTab: HydratedSavedGameSnapshot['bottomTab'] = 'adventure',
): HydratedSavedGameSnapshot {
const gameState = {
...(projection.gameState as unknown as GameState),
runtimeSessionId: projection.storySession.runtimeSessionId,
storySessionId: projection.storySession.storySessionId,
runtimeActionVersion: projection.serverVersion,
} satisfies GameState;
const currentStory = buildStoryMomentFromRuntimeProjection({
projection,
gameState,
});
// 中文注释:新写入接口只返回 story runtime 投影,前端在边界层
// 还原为已有运行时快照格式,避免下游 hooks 继续认识旧 action response。
return rehydrateSavedSnapshot({
version: projection.serverVersion,
savedAt: projection.storySession.updatedAt,
bottomTab,
gameState,
currentStory,
} as HydratedSavedGameSnapshot);
}
function normalizeRuntimeMutationResponse(
response: StoryRuntimeMutationResponse,
): RuntimeStoryResponse {
const { projection } = response;
const snapshot = buildRuntimeSnapshotFromProjection(projection);
return {
sessionId: projection.storySession.runtimeSessionId,
serverVersion: projection.serverVersion,
projection,
snapshot,
inventoryView: mapRuntimeProjectionInventory(projection),
};
}
export function getRuntimeSessionId(
gameState: Pick<GameState, 'runtimeSessionId'>,
) {
@@ -279,18 +305,13 @@ export function resolveRuntimeStoryMoment(params: {
return params.hydratedSnapshot.currentStory!;
}
const options =
params.response.viewModel.availableOptions.length > 0
? params.response.viewModel.availableOptions
: params.response.presentation.options;
return buildStoryMomentFromRuntimeOptions({
storyText:
params.response.presentation.storyText ||
params.response.presentation?.storyText ||
params.hydratedSnapshot.currentStory?.text ||
params.fallbackStoryText ||
'',
options,
options: params.response.projection.options.map(mapRuntimeProjectionOption),
gameState: params.hydratedSnapshot.gameState.currentEncounter
? params.hydratedSnapshot.gameState
: params.fallbackGameState,
@@ -408,14 +429,14 @@ export async function loadRuntimeInventoryView(
}
export async function beginRuntimeStorySession(
params: RuntimeStoryBootstrapRequest<
params: BeginStoryRuntimeSessionRequest<
GameState['customWorldProfile'],
NonNullable<GameState['playerCharacter']>
>,
options: RpgRuntimeStoryClientOptions = {},
) {
const response = await requestRuntimeStoryJson<RuntimeStoryBootstrapResult>(
'/sessions',
const response = await requestStorySessionJson<StoryRuntimeMutationResponse>(
'/runtime',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -425,17 +446,12 @@ export async function beginRuntimeStorySession(
options,
);
return {
...response,
snapshot: rehydrateSavedSnapshot(
response.snapshot as HydratedSavedGameSnapshot,
),
} satisfies RuntimeStoryBootstrapResult;
return normalizeRuntimeMutationResponse(response);
}
export async function resolveRuntimeStoryAction(
params: {
sessionId?: string;
storySessionId: string;
clientVersion?: number;
option: Pick<StoryOption, 'functionId' | 'actionText'>;
targetId?: string;
@@ -443,76 +459,36 @@ export async function resolveRuntimeStoryAction(
},
options: RpgRuntimeStoryClientOptions = {},
) {
// 中文注释story_choice 是当前前端统一提交给服务端的动作包裹格式,
// optionText 会一起带上,方便服务端日志、提示词和调试链查看用户当轮选择。
const response = await requestRuntimeStoryJson<RuntimeStoryResponse>(
'/actions/resolve',
const storySessionId = normalizeStorySessionId(
params.storySessionId,
'故事会话不存在,无法执行运行时动作',
);
// 中文注释:写入 DTO 采用 story session scoped 扁平字段;
// optionText 仍放进 payload方便服务端日志、提示词和调试链查看用户当轮选择。
const response = await requestStorySessionJson<StoryRuntimeMutationResponse>(
`/${encodeURIComponent(storySessionId)}/actions/resolve`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionId: params.sessionId || DEFAULT_SESSION_ID,
storySessionId,
clientVersion: params.clientVersion,
action: {
type: 'story_choice',
functionId: params.option.functionId,
targetId: params.targetId,
payload: {
optionText: params.option.actionText,
...(params.payload ?? {}),
},
functionId: params.option.functionId,
actionText: params.option.actionText,
targetId: params.targetId,
payload: {
optionText: params.option.actionText,
...(params.payload ?? {}),
},
} satisfies RuntimeStoryActionRequest),
} satisfies ResolveStoryRuntimeActionRequest),
},
'执行运行时动作失败',
options,
);
return {
...response,
snapshot: rehydrateSavedSnapshot(
response.snapshot as HydratedSavedGameSnapshot,
),
} satisfies RuntimeStoryResponse;
return normalizeRuntimeMutationResponse(response);
}
export function getRuntimeActionSnapshot(response: RuntimeStoryResponse) {
return rehydrateSavedSnapshot(response.snapshot as HydratedSavedGameSnapshot);
return response.snapshot;
}
export const beginRpgRuntimeStorySession = beginRuntimeStorySession;
export const beginRpgStorySession = beginStorySession;
export const continueRpgStorySession = continueStorySession;
export const getRpgStoryRuntimeProjection = getStoryRuntimeProjection;
export const getRpgStorySessionState = getStorySessionState;
export const getRpgRuntimeActionSnapshot = getRuntimeActionSnapshot;
export const getRpgRuntimeClientVersion = getRuntimeClientVersion;
export const getRpgRuntimeSessionId = getRuntimeSessionId;
export const getRpgRuntimeStorySessionId = getRuntimeStorySessionId;
export const getRpgRuntimeStoryState = getRuntimeStoryState;
export const isRpgRuntimeServerFunctionId = isServerRuntimeFunctionId;
export const isRpgRuntimeTaskFunctionId = isTask5RuntimeFunctionId;
export const loadRpgRuntimeInventoryView = loadRuntimeInventoryView;
export const resolveRpgRuntimeStoryAction = resolveRuntimeStoryAction;
export const resolveRpgRuntimeStoryProjectionMoment =
buildStoryMomentFromRuntimeProjection;
export const resolveRpgRuntimeStoryMoment = resolveRuntimeStoryMoment;
export const shouldUseRpgRuntimeServerOptions = shouldUseServerRuntimeOptions;
export const rpgRuntimeStoryClient = {
beginSession: beginRpgRuntimeStorySession,
beginStorySession: beginRpgStorySession,
continueStorySession: continueRpgStorySession,
getActionSnapshot: getRpgRuntimeActionSnapshot,
getClientVersion: getRpgRuntimeClientVersion,
getInventoryView: loadRpgRuntimeInventoryView,
getSessionId: getRpgRuntimeSessionId,
getStoryRuntimeProjection: getRpgStoryRuntimeProjection,
getStorySessionId: getRpgRuntimeStorySessionId,
getStorySessionState: getRpgStorySessionState,
getState: getRpgRuntimeStoryState,
resolveAction: resolveRpgRuntimeStoryAction,
resolveProjectionMoment: resolveRpgRuntimeStoryProjectionMoment,
resolveMoment: resolveRpgRuntimeStoryMoment,
shouldUseServerOptions: shouldUseRpgRuntimeServerOptions,
};