Merge branch 'codex/dev' into codex/backend-rewrite-spacetimedb

# Conflicts:
#	docs/technical/README.md
#	server-node/src/modules/assets/qwenSpriteRoutes.ts
#	src/components/CustomWorldResultView.test.tsx
#	src/components/CustomWorldResultView.tsx
#	src/components/custom-world-agent/CustomWorldAgentDraftDetailPanel.tsx
#	src/components/game-shell/PreGameSelectionFlow.agent.interaction.test.tsx
#	src/components/rpg-creation-asset-studio/RpgCreationRoleAssetStudioModalImpl.tsx
#	src/components/rpg-creation-editor/RpgCreationEntityEditorShared.tsx
#	src/components/rpg-entry/RpgEntryCharacterSelectView.tsx
#	src/components/rpg-entry/RpgEntryHomeView.tsx
#	src/services/apiClient.ts
#	src/tools/QwenSpriteSheetTool.tsx
This commit is contained in:
2026-04-21 20:16:01 +08:00
477 changed files with 38047 additions and 26570 deletions

View File

@@ -78,7 +78,8 @@ export type AuthPhoneChangeResponse = {
};
export type AuthRefreshResponse = {
token: string;
ok: true;
token?: string;
};
export type AuthSessionSummary = {

View File

@@ -1,533 +1,12 @@
export type CustomWorldWorkStatus = 'draft' | 'published';
export type CustomWorldWorkSource = 'agent_session' | 'published_profile';
/**
* 兼容出口:
* 当前仓库仍有大量旧 customWorld 命名导入,这个文件继续作为过渡层保留。
* 工作包 H 完成后,真实类型定义已经迁移到 rpg* 契约文件中;这里仅聚合旧命名分文件。
*/
export interface WorldPromiseValue {
hook: string;
differentiator: string;
desiredExperience: string;
}
export interface PlayerFantasyValue {
playerRole: string;
corePursuit: string;
fearOfLoss: string;
}
export interface ThemeBoundaryValue {
toneKeywords: string[];
aestheticDirectives: string[];
forbiddenDirectives: string[];
}
export interface PlayerEntryPointValue {
openingIdentity: string;
openingProblem: string;
entryMotivation: string;
}
export interface CoreConflictValue {
surfaceConflicts: string[];
hiddenCrisis: string;
firstTouchedConflict: string;
}
export interface KeyRelationshipValue {
pairs: string;
relationshipType: string;
secretOrCost: string;
}
export interface HiddenLineValue {
hiddenTruths: string[];
misdirectionHints: string[];
revealPacing: string;
}
export interface IconicElementValue {
iconicMotifs: string[];
institutionsOrArtifacts: string[];
hardRules: string[];
}
export interface EightAnchorContent {
worldPromise: WorldPromiseValue | null;
playerFantasy: PlayerFantasyValue | null;
themeBoundary: ThemeBoundaryValue | null;
playerEntryPoint: PlayerEntryPointValue | null;
coreConflict: CoreConflictValue | null;
keyRelationships: KeyRelationshipValue[];
hiddenLines: HiddenLineValue | null;
iconicElements: IconicElementValue | null;
}
export interface CustomWorldWorkSummary {
workId: string;
sourceType: CustomWorldWorkSource;
status: CustomWorldWorkStatus;
title: string;
subtitle: string;
summary: string;
coverImageSrc?: string | null;
coverRenderMode?: 'image' | 'scene_with_roles';
coverCharacterImageSrcs?: string[];
updatedAt: string;
publishedAt?: string | null;
stage?: string | null;
stageLabel?: string | null;
playableNpcCount: number;
landmarkCount: number;
roleVisualReadyCount?: number;
roleAnimationReadyCount?: number;
roleAssetSummaryLabel?: string | null;
sessionId?: string | null;
profileId?: string | null;
canResume: boolean;
canEnterWorld: boolean;
}
export interface CreatorIntentReadiness {
isReady: boolean;
completedKeys: string[];
missingKeys: string[];
}
export interface CustomWorldPendingClarification {
id: string;
label: string;
question: string;
targetKey:
| 'world_hook'
| 'player_premise'
| 'theme_and_tone'
| 'core_conflict'
| 'relationship_seed'
| 'iconic_element';
priority: number;
answer?: string;
}
export type CustomWorldAgentStage =
| 'collecting_intent'
| 'clarifying'
| 'foundation_review'
| 'object_refining'
| 'visual_refining'
| 'long_tail_review'
| 'ready_to_publish'
| 'published'
| 'error';
export type CustomWorldAgentMessageRole = 'user' | 'assistant' | 'system';
export type CustomWorldAgentMessageKind =
| 'chat'
| 'clarification'
| 'summary'
| 'checkpoint'
| 'warning'
| 'action_result';
export interface CustomWorldAgentMessage {
id: string;
role: CustomWorldAgentMessageRole;
kind: CustomWorldAgentMessageKind;
text: string;
createdAt: string;
relatedOperationId?: string | null;
}
export type CustomWorldDraftCardKind =
| 'world'
| 'camp'
| 'faction'
| 'character'
| 'landmark'
| 'thread'
| 'chapter'
| 'scene_chapter'
| 'carrier'
| 'sidequest_seed';
export type CustomWorldDraftCardStatus =
| 'suggested'
| 'confirmed'
| 'locked'
| 'warning';
export interface CustomWorldDraftCardSummary {
id: string;
kind: CustomWorldDraftCardKind;
title: string;
subtitle: string;
summary: string;
status: CustomWorldDraftCardStatus;
linkedIds: string[];
warningCount: number;
assetStatus?: CustomWorldRoleAssetStatus | null;
assetStatusLabel?: string | null;
}
export interface CustomWorldDraftCardDetailSection {
id: string;
label: string;
value: string;
}
export interface CustomWorldFoundationDraftFaction {
id: string;
name: string;
title?: string;
subtitle?: string;
publicGoal: string;
relatedConflict: string;
tension?: string;
playerRelation: string;
summary: string;
}
export interface CustomWorldFoundationDraftCharacter {
id: string;
name: string;
title: string;
role: string;
publicIdentity: string;
publicMask?: string;
currentPressure: string;
hiddenHook?: string;
relationToPlayer: string;
threadIds: string[];
summary: string;
skills?: Array<{
id: string;
name: string;
actionPreviewConfig?: Record<string, unknown> | null;
}>;
imageSrc?: string | null;
generatedVisualAssetId?: string | null;
generatedAnimationSetId?: string | null;
animationMap?: Record<string, unknown> | null;
}
export interface CustomWorldFoundationDraftLandmark {
id: string;
name: string;
description?: string;
purpose: string;
mood: string;
importance: string;
secret?: string;
dangerLevel?: string;
imageSrc?: string | null;
characterIds: string[];
threadIds: string[];
summary: string;
}
export interface CustomWorldFoundationDraftThread {
id: string;
title: string;
type: 'main' | 'hidden';
conflictType?: string;
conflict: string;
stakes?: string;
characterIds: string[];
landmarkIds: string[];
summary: string;
}
export interface CustomWorldFoundationDraftChapter {
id: string;
title: string;
openingEvent: string;
playerGoal: string;
characterIds: string[];
landmarkIds: string[];
understandingShift: string;
summary: string;
}
export interface CustomWorldFoundationDraftCamp {
id: string;
name: string;
description: string;
mood: string;
dangerLevel?: string;
imageSrc?: string | null;
summary: string;
}
export type CustomWorldSceneActStage =
| 'opening'
| 'expansion'
| 'turning_point'
| 'climax'
| 'aftermath';
export type CustomWorldSceneActAdvanceRule =
| 'after_primary_contact'
| 'after_active_step_complete'
| 'after_chapter_resolution';
export interface CustomWorldFoundationDraftSceneAct {
id: string;
title: string;
summary: string;
stageCoverage: CustomWorldSceneActStage[];
backgroundImageSrc?: string | null;
backgroundAssetId?: string | null;
encounterNpcIds: string[];
primaryNpcId: string;
linkedThreadIds: string[];
actGoal: string;
transitionHook: string;
advanceRule: CustomWorldSceneActAdvanceRule;
}
export interface CustomWorldFoundationDraftSceneChapter {
id: string;
sceneId: string;
sceneName: string;
title: string;
summary: string;
linkedThreadIds: string[];
linkedLandmarkIds: string[];
acts: CustomWorldFoundationDraftSceneAct[];
}
export interface CustomWorldFoundationDraftProfile {
name: string;
subtitle: string;
summary: string;
tone: string;
playerGoal: string;
majorFactions: string[];
coreConflicts: string[];
playableNpcs: CustomWorldFoundationDraftCharacter[];
storyNpcs: CustomWorldFoundationDraftCharacter[];
landmarks: CustomWorldFoundationDraftLandmark[];
camp?: CustomWorldFoundationDraftCamp | null;
themePack?: Record<string, unknown> | null;
storyGraph?: Record<string, unknown> | null;
factions: CustomWorldFoundationDraftFaction[];
threads: CustomWorldFoundationDraftThread[];
chapters: CustomWorldFoundationDraftChapter[];
sceneChapters: CustomWorldFoundationDraftSceneChapter[];
worldHook: string;
playerPremise: string;
openingSituation: string;
iconicElements: string[];
sourceAnchorSummary: string;
}
export interface CustomWorldFoundationDraftResult {
draftProfile: CustomWorldFoundationDraftProfile;
draftCards: CustomWorldDraftCardSummary[];
}
export interface CustomWorldDraftCardDetail {
id: string;
kind: CustomWorldDraftCardKind;
title: string;
sections: CustomWorldDraftCardDetailSection[];
linkedIds: string[];
locked: false;
editable: boolean;
editableSectionIds: string[];
warningMessages: string[];
assetStatus?: CustomWorldRoleAssetStatus | null;
assetStatusLabel?: string | null;
}
export interface CustomWorldSuggestedAction {
id: string;
type:
| 'request_summary'
| 'draft_foundation'
| 'refine_focus_target'
| 'lock_current_target'
| 'generate_role_assets'
| 'generate_scene_assets'
| 'expand_long_tail'
| 'publish_world';
label: string;
targetId?: string | null;
}
export type CustomWorldAssetPriorityTier = 'hero' | 'featured' | 'supporting';
export type CustomWorldRoleAssetStatus =
| 'missing'
| 'visual_ready'
| 'animations_ready'
| 'complete';
export interface CustomWorldRoleAssetSummary {
roleId: string;
roleName: string;
roleKind: 'playable' | 'story';
priorityTier: CustomWorldAssetPriorityTier;
portraitPath?: string | null;
generatedVisualAssetId?: string | null;
generatedAnimationSetId?: string | null;
status: CustomWorldRoleAssetStatus;
missingAnimations: string[];
nextPointCost: number;
}
export interface CustomWorldSceneAssetSummary {
sceneId: string;
sceneName: string;
actId?: string | null;
actTitle?: string | null;
imageSrc?: string | null;
assetId?: string | null;
status: 'missing' | 'ready';
nextPointCost: number;
}
export interface CustomWorldAssetCoverageSummary {
roleAssets: CustomWorldRoleAssetSummary[];
sceneAssets: CustomWorldSceneAssetSummary[];
allRoleAssetsReady: boolean;
allSceneAssetsReady: boolean;
}
export interface CustomWorldAgentSessionSnapshot {
sessionId: string;
currentTurn: number;
anchorContent: EightAnchorContent;
progressPercent: number;
lastAssistantReply: string | null;
stage: CustomWorldAgentStage;
focusCardId: string | null;
creatorIntent: Record<string, unknown> | null;
creatorIntentReadiness: CreatorIntentReadiness;
anchorPack: Record<string, unknown> | null;
lockState: Record<string, unknown> | null;
draftProfile: Record<string, unknown> | null;
messages: CustomWorldAgentMessage[];
draftCards: CustomWorldDraftCardSummary[];
pendingClarifications: CustomWorldPendingClarification[];
suggestedActions: CustomWorldSuggestedAction[];
recommendedReplies: string[];
qualityFindings: {
id: string;
severity: 'info' | 'warning' | 'blocker';
code: string;
targetId?: string | null;
message: string;
}[];
assetCoverage: CustomWorldAssetCoverageSummary;
updatedAt: string;
}
export type CustomWorldAgentOperationType =
| 'process_message'
| 'lock_cards'
| 'unlock_cards'
| 'regenerate_scope'
| 'draft_foundation'
| 'update_draft_card'
| 'generate_characters'
| 'generate_landmarks'
| 'generate_role_assets'
| 'sync_role_assets'
| 'generate_scene_assets'
| 'sync_scene_assets'
| 'expand_long_tail'
| 'publish_world'
| 'revert_checkpoint';
export type CustomWorldAgentOperationStatus =
| 'queued'
| 'running'
| 'completed'
| 'failed';
export interface CustomWorldAgentOperationRecord {
operationId: string;
type: CustomWorldAgentOperationType;
status: CustomWorldAgentOperationStatus;
phaseLabel: string;
phaseDetail: string;
progress: number;
error?: string | null;
}
export interface CreateCustomWorldAgentSessionRequest {
seedText?: string;
}
export interface CreateCustomWorldAgentSessionResponse {
session: CustomWorldAgentSessionSnapshot;
}
export interface SendCustomWorldAgentMessageRequest {
clientMessageId: string;
text: string;
quickFillRequested?: boolean;
focusCardId?: string | null;
selectedCardIds?: string[];
}
export interface SendCustomWorldAgentMessageResponse {
operation: CustomWorldAgentOperationRecord;
}
export type CustomWorldAgentActionRequest =
| { action: 'lock_cards'; cardIds: string[] }
| { action: 'unlock_cards'; cardIds: string[] }
| {
action: 'regenerate_scope';
scope:
| 'focus_card'
| 'long_tail_npcs'
| 'long_tail_landmarks'
| 'sidequest_seeds'
| 'role_assets'
| 'scene_assets';
targetCardId?: string | null;
}
| { action: 'draft_foundation' }
| {
action: 'update_draft_card';
cardId: string;
sections: Array<{
sectionId: string;
value: string;
}>;
}
| {
action: 'generate_characters';
count: number;
promptText?: string | null;
anchorCardIds?: string[];
}
| {
action: 'generate_landmarks';
count: number;
promptText?: string | null;
anchorCardIds?: string[];
}
| { action: 'generate_role_assets'; roleIds: string[] }
| {
action: 'sync_role_assets';
roleId: string;
portraitPath: string;
generatedVisualAssetId: string;
generatedAnimationSetId?: string | null;
animationMap?: Record<string, unknown> | null;
}
| { action: 'publish_world' };
export interface CustomWorldAgentActionResponse {
operation: CustomWorldAgentOperationRecord;
}
export interface GetCustomWorldAgentCardDetailResponse {
card: CustomWorldDraftCardDetail;
}
export interface ListCustomWorldWorksResponse {
items: CustomWorldWorkSummary[];
}
export type * from './customWorldAgentAnchors';
export type * from './customWorldAgentDraft';
export type * from './customWorldAgentActions';
export type * from './customWorldAgentSession';
export type * from './customWorldResultPreview';
export type * from './customWorldWorkSummary';

View File

@@ -0,0 +1,14 @@
/**
* 旧 custom world 动作契约兼容出口。
* 后续若逐步迁移旧代码,建议直接改用 rpgAgentActions.ts。
*/
export type {
RpgAgentActionRequest as CustomWorldAgentActionRequest,
RpgAgentActionResponse as CustomWorldAgentActionResponse,
RpgAgentOperationRecord as CustomWorldAgentOperationRecord,
RpgAgentOperationStatus as CustomWorldAgentOperationStatus,
RpgAgentOperationType as CustomWorldAgentOperationType,
RpgAgentSupportedAction as CustomWorldSupportedAction,
RpgAgentSuggestedAction as CustomWorldSuggestedAction,
} from './rpgAgentActions';

View File

@@ -0,0 +1,16 @@
/**
* 旧 custom world 八锚点兼容出口。
* 这里只保留旧命名到 RPG 创作域新契约的映射,便于旧导入渐进迁移。
*/
export type {
RpgCreationAnchorContent as EightAnchorContent,
RpgCreationCoreConflictValue as CoreConflictValue,
RpgCreationHiddenLineValue as HiddenLineValue,
RpgCreationIconicElementValue as IconicElementValue,
RpgCreationKeyRelationshipValue as KeyRelationshipValue,
RpgCreationPlayerEntryPointValue as PlayerEntryPointValue,
RpgCreationPlayerFantasyValue as PlayerFantasyValue,
RpgCreationThemeBoundaryValue as ThemeBoundaryValue,
RpgCreationWorldPromiseValue as WorldPromiseValue,
} from './rpgAgentAnchors';

View File

@@ -0,0 +1,29 @@
/**
* 旧 custom world 草稿契约兼容出口。
* 工作包 H 完成后,真实定义已经迁到 rpgAgentDraft.ts这里只负责旧命名映射。
*/
export type {
RpgAgentAssetCoverageSummary as CustomWorldAssetCoverageSummary,
RpgAgentAssetPriorityTier as CustomWorldAssetPriorityTier,
RpgAgentDraftCardDetail as CustomWorldDraftCardDetail,
RpgAgentDraftCardDetailSection as CustomWorldDraftCardDetailSection,
RpgAgentDraftCardKind as CustomWorldDraftCardKind,
RpgAgentDraftCardStatus as CustomWorldDraftCardStatus,
RpgAgentDraftCardSummary as CustomWorldDraftCardSummary,
RpgAgentFoundationDraftCamp as CustomWorldFoundationDraftCamp,
RpgAgentFoundationDraftChapter as CustomWorldFoundationDraftChapter,
RpgAgentFoundationDraftCharacter as CustomWorldFoundationDraftCharacter,
RpgAgentFoundationDraftFaction as CustomWorldFoundationDraftFaction,
RpgAgentFoundationDraftLandmark as CustomWorldFoundationDraftLandmark,
RpgAgentFoundationDraftProfile as CustomWorldFoundationDraftProfile,
RpgAgentFoundationDraftResult as CustomWorldFoundationDraftResult,
RpgAgentFoundationDraftSceneAct as CustomWorldFoundationDraftSceneAct,
RpgAgentFoundationDraftSceneChapter as CustomWorldFoundationDraftSceneChapter,
RpgAgentFoundationDraftThread as CustomWorldFoundationDraftThread,
RpgAgentRoleAssetStatus as CustomWorldRoleAssetStatus,
RpgAgentRoleAssetSummary as CustomWorldRoleAssetSummary,
RpgAgentSceneActAdvanceRule as CustomWorldSceneActAdvanceRule,
RpgAgentSceneActStage as CustomWorldSceneActStage,
RpgAgentSceneAssetSummary as CustomWorldSceneAssetSummary,
} from './rpgAgentDraft';

View File

@@ -0,0 +1,20 @@
/**
* 旧 custom world 会话契约兼容出口。
* 这一层只做命名映射,不再承担 session 真相源结构定义。
*/
export type {
CreateRpgAgentSessionRequest as CreateCustomWorldAgentSessionRequest,
CreateRpgAgentSessionResponse as CreateCustomWorldAgentSessionResponse,
GetRpgAgentCardDetailResponse as GetCustomWorldAgentCardDetailResponse,
RpgAgentMessage as CustomWorldAgentMessage,
RpgAgentMessageKind as CustomWorldAgentMessageKind,
RpgAgentMessageRole as CustomWorldAgentMessageRole,
RpgAgentPendingClarification as CustomWorldPendingClarification,
RpgAgentQualityFinding as CustomWorldAgentQualityFinding,
RpgAgentSessionSnapshot as CustomWorldAgentSessionSnapshot,
RpgAgentStage as CustomWorldAgentStage,
RpgCreationIntentReadiness as CreatorIntentReadiness,
SendRpgAgentMessageRequest as SendCustomWorldAgentMessageRequest,
SendRpgAgentMessageResponse as SendCustomWorldAgentMessageResponse,
} from './rpgAgentSession';

View File

@@ -0,0 +1,12 @@
/**
* 旧 custom world 结果页预览兼容出口。
* 额外单独拆一个 preview 兼容文件,避免预览别名继续堆回 customWorldAgent.ts 聚合层。
*/
export type {
RpgCreationPreview as CustomWorldResultPreview,
RpgCreationPreviewBlocker as CustomWorldResultPreviewBlocker,
RpgCreationPreviewEnvelope as CustomWorldResultPreviewEnvelope,
RpgCreationPreviewFinding as CustomWorldResultPreviewFinding,
RpgCreationPreviewSource as CustomWorldResultPreviewSource,
} from './rpgCreationPreview';

View File

@@ -0,0 +1,11 @@
/**
* 旧 custom world works 读模型兼容出口。
* 用于把旧作品列表命名平滑映射到新的 RPG 创作域 works 契约。
*/
export type {
ListRpgCreationWorksResponse as ListCustomWorldWorksResponse,
RpgCreationWorkSource as CustomWorldWorkSource,
RpgCreationWorkStatus as CustomWorldWorkStatus,
RpgCreationWorkSummary as CustomWorldWorkSummary,
} from './rpgCreationWorkSummary';

View File

@@ -0,0 +1,120 @@
/**
* RPG Agent 动作与异步操作契约。
* 这里显式区分“建议动作”和“真实可执行动作”,为后续后端 registry 收口预留接口。
*/
export type RpgAgentSuggestedActionType =
| 'request_summary'
| 'draft_foundation'
| 'refine_focus_target'
| 'lock_current_target'
| 'generate_role_assets'
| 'generate_scene_assets'
| 'expand_long_tail'
| 'publish_world';
export interface RpgAgentSuggestedAction {
id: string;
type: RpgAgentSuggestedActionType;
label: string;
targetId?: string | null;
}
export type RpgAgentActionType =
| 'draft_foundation'
| 'update_draft_card'
| 'sync_result_profile'
| 'generate_characters'
| 'generate_landmarks'
| 'generate_role_assets'
| 'sync_role_assets'
| 'generate_scene_assets'
| 'sync_scene_assets'
| 'expand_long_tail'
| 'publish_world'
| 'revert_checkpoint';
export type RpgAgentActionCapabilityKey =
| RpgAgentSuggestedActionType
| RpgAgentActionType;
/**
* 当前先把能力矩阵定义为可选契约。
* 等工作包 E 的 registry 落地后,后端可以把真实 supportedActions 填充到 session snapshot。
*/
export interface RpgAgentSupportedAction {
action: RpgAgentActionCapabilityKey;
enabled: boolean;
reason?: string | null;
}
export type RpgAgentOperationType = RpgAgentActionType | 'process_message';
export type RpgAgentOperationStatus =
| 'queued'
| 'running'
| 'completed'
| 'failed';
export interface RpgAgentOperationRecord {
operationId: string;
type: RpgAgentOperationType;
status: RpgAgentOperationStatus;
phaseLabel: string;
phaseDetail: string;
progress: number;
error?: string | null;
}
export type RpgAgentActionRequest =
| { action: 'draft_foundation' }
| {
action: 'update_draft_card';
cardId: string;
sections: Array<{
sectionId: string;
value: string;
}>;
}
| {
action: 'sync_result_profile';
profile: Record<string, unknown>;
}
| {
action: 'generate_characters';
count: number;
promptText?: string | null;
anchorCardIds?: string[];
}
| {
action: 'generate_landmarks';
count: number;
promptText?: string | null;
anchorCardIds?: string[];
}
| { action: 'generate_role_assets'; roleIds: string[] }
| {
action: 'sync_role_assets';
roleId: string;
portraitPath: string;
generatedVisualAssetId: string;
generatedAnimationSetId?: string | null;
animationMap?: Record<string, unknown> | null;
}
| { action: 'generate_scene_assets'; sceneIds: string[] }
| {
action: 'sync_scene_assets';
sceneId: string;
sceneKind: 'camp' | 'landmark';
imageSrc: string;
generatedSceneAssetId: string;
generatedScenePrompt?: string | null;
generatedSceneModel?: string | null;
}
| { action: 'expand_long_tail' }
| { action: 'publish_world' }
| { action: 'revert_checkpoint'; checkpointId: string };
export interface RpgAgentActionResponse {
operation: RpgAgentOperationRecord;
}

View File

@@ -0,0 +1,63 @@
/**
* RPG 创作八锚点契约。
* 这一层只描述“创作意图采集态”的结构,不混入 session 或结果页字段。
*/
export interface RpgCreationWorldPromiseValue {
hook: string;
differentiator: string;
desiredExperience: string;
}
export interface RpgCreationPlayerFantasyValue {
playerRole: string;
corePursuit: string;
fearOfLoss: string;
}
export interface RpgCreationThemeBoundaryValue {
toneKeywords: string[];
aestheticDirectives: string[];
forbiddenDirectives: string[];
}
export interface RpgCreationPlayerEntryPointValue {
openingIdentity: string;
openingProblem: string;
entryMotivation: string;
}
export interface RpgCreationCoreConflictValue {
surfaceConflicts: string[];
hiddenCrisis: string;
firstTouchedConflict: string;
}
export interface RpgCreationKeyRelationshipValue {
pairs: string;
relationshipType: string;
secretOrCost: string;
}
export interface RpgCreationHiddenLineValue {
hiddenTruths: string[];
misdirectionHints: string[];
revealPacing: string;
}
export interface RpgCreationIconicElementValue {
iconicMotifs: string[];
institutionsOrArtifacts: string[];
hardRules: string[];
}
export interface RpgCreationAnchorContent {
worldPromise: RpgCreationWorldPromiseValue | null;
playerFantasy: RpgCreationPlayerFantasyValue | null;
themeBoundary: RpgCreationThemeBoundaryValue | null;
playerEntryPoint: RpgCreationPlayerEntryPointValue | null;
coreConflict: RpgCreationCoreConflictValue | null;
keyRelationships: RpgCreationKeyRelationshipValue[];
hiddenLines: RpgCreationHiddenLineValue | null;
iconicElements: RpgCreationIconicElementValue | null;
}

View File

@@ -0,0 +1,251 @@
/**
* RPG Agent 草稿与资产覆盖率契约。
* 这一层只描述 foundation draft、草稿卡片与资产状态不包含会话编排语义。
*/
export type RpgAgentDraftCardKind =
| 'world'
| 'camp'
| 'faction'
| 'character'
| 'landmark'
| 'thread'
| 'chapter'
| 'scene_chapter'
| 'carrier'
| 'sidequest_seed';
export type RpgAgentDraftCardStatus =
| 'suggested'
| 'confirmed'
| 'locked'
| 'warning';
export interface RpgAgentDraftCardSummary {
id: string;
kind: RpgAgentDraftCardKind;
title: string;
subtitle: string;
summary: string;
status: RpgAgentDraftCardStatus;
linkedIds: string[];
warningCount: number;
assetStatus?: RpgAgentRoleAssetStatus | null;
assetStatusLabel?: string | null;
}
export interface RpgAgentDraftCardDetailSection {
id: string;
label: string;
value: string;
}
export interface RpgAgentFoundationDraftFaction {
id: string;
name: string;
title?: string;
subtitle?: string;
publicGoal: string;
relatedConflict: string;
tension?: string;
playerRelation: string;
summary: string;
}
export interface RpgAgentFoundationDraftCharacter {
id: string;
name: string;
title: string;
role: string;
publicIdentity: string;
publicMask?: string;
currentPressure: string;
hiddenHook?: string;
relationToPlayer: string;
threadIds: string[];
summary: string;
skills?: Array<{
id: string;
name: string;
actionPreviewConfig?: Record<string, unknown> | null;
}>;
imageSrc?: string | null;
generatedVisualAssetId?: string | null;
generatedAnimationSetId?: string | null;
animationMap?: Record<string, unknown> | null;
}
export interface RpgAgentFoundationDraftLandmark {
id: string;
name: string;
description?: string;
purpose: string;
mood: string;
importance: string;
secret?: string;
dangerLevel?: string;
imageSrc?: string | null;
generatedSceneAssetId?: string | null;
generatedScenePrompt?: string | null;
generatedSceneModel?: string | null;
characterIds: string[];
threadIds: string[];
summary: string;
}
export interface RpgAgentFoundationDraftThread {
id: string;
title: string;
type: 'main' | 'hidden';
conflictType?: string;
conflict: string;
stakes?: string;
characterIds: string[];
landmarkIds: string[];
summary: string;
}
export interface RpgAgentFoundationDraftChapter {
id: string;
title: string;
openingEvent: string;
playerGoal: string;
characterIds: string[];
landmarkIds: string[];
understandingShift: string;
summary: string;
}
export interface RpgAgentFoundationDraftCamp {
id: string;
name: string;
description: string;
mood: string;
dangerLevel?: string;
imageSrc?: string | null;
generatedSceneAssetId?: string | null;
generatedScenePrompt?: string | null;
generatedSceneModel?: string | null;
summary: string;
}
export type RpgAgentSceneActStage =
| 'opening'
| 'expansion'
| 'turning_point'
| 'climax'
| 'aftermath';
export type RpgAgentSceneActAdvanceRule =
| 'after_primary_contact'
| 'after_active_step_complete'
| 'after_chapter_resolution';
export interface RpgAgentFoundationDraftSceneAct {
id: string;
title: string;
summary: string;
stageCoverage: RpgAgentSceneActStage[];
backgroundImageSrc?: string | null;
backgroundAssetId?: string | null;
encounterNpcIds: string[];
primaryNpcId: string;
linkedThreadIds: string[];
actGoal: string;
transitionHook: string;
advanceRule: RpgAgentSceneActAdvanceRule;
}
export interface RpgAgentFoundationDraftSceneChapter {
id: string;
sceneId: string;
sceneName: string;
title: string;
summary: string;
linkedThreadIds: string[];
linkedLandmarkIds: string[];
acts: RpgAgentFoundationDraftSceneAct[];
}
export interface RpgAgentFoundationDraftProfile {
name: string;
subtitle: string;
summary: string;
tone: string;
playerGoal: string;
majorFactions: string[];
coreConflicts: string[];
playableNpcs: RpgAgentFoundationDraftCharacter[];
storyNpcs: RpgAgentFoundationDraftCharacter[];
landmarks: RpgAgentFoundationDraftLandmark[];
camp?: RpgAgentFoundationDraftCamp | null;
themePack?: Record<string, unknown> | null;
storyGraph?: Record<string, unknown> | null;
factions: RpgAgentFoundationDraftFaction[];
threads: RpgAgentFoundationDraftThread[];
chapters: RpgAgentFoundationDraftChapter[];
sceneChapters: RpgAgentFoundationDraftSceneChapter[];
worldHook: string;
playerPremise: string;
openingSituation: string;
iconicElements: string[];
sourceAnchorSummary: string;
}
export interface RpgAgentFoundationDraftResult {
draftProfile: RpgAgentFoundationDraftProfile;
draftCards: RpgAgentDraftCardSummary[];
}
export interface RpgAgentDraftCardDetail {
id: string;
kind: RpgAgentDraftCardKind;
title: string;
sections: RpgAgentDraftCardDetailSection[];
linkedIds: string[];
locked: false;
editable: boolean;
editableSectionIds: string[];
warningMessages: string[];
assetStatus?: RpgAgentRoleAssetStatus | null;
assetStatusLabel?: string | null;
}
export type RpgAgentAssetPriorityTier = 'hero' | 'featured' | 'supporting';
export type RpgAgentRoleAssetStatus =
| 'missing'
| 'visual_ready'
| 'animations_ready'
| 'complete';
export interface RpgAgentRoleAssetSummary {
roleId: string;
roleName: string;
roleKind: 'playable' | 'story';
priorityTier: RpgAgentAssetPriorityTier;
portraitPath?: string | null;
generatedVisualAssetId?: string | null;
generatedAnimationSetId?: string | null;
status: RpgAgentRoleAssetStatus;
missingAnimations: string[];
nextPointCost: number;
}
export interface RpgAgentSceneAssetSummary {
sceneId: string;
sceneName: string;
actId?: string | null;
actTitle?: string | null;
imageSrc?: string | null;
assetId?: string | null;
status: 'missing' | 'ready';
nextPointCost: number;
}
export interface RpgAgentAssetCoverageSummary {
roleAssets: RpgAgentRoleAssetSummary[];
sceneAssets: RpgAgentSceneAssetSummary[];
allRoleAssetsReady: boolean;
allSceneAssetsReady: boolean;
}

View File

@@ -0,0 +1,134 @@
import type { RpgAgentActionResponse, RpgAgentOperationRecord, RpgAgentSupportedAction, RpgAgentSuggestedAction } from './rpgAgentActions';
import type { RpgCreationAnchorContent } from './rpgAgentAnchors';
import type { RpgAgentAssetCoverageSummary, RpgAgentDraftCardDetail, RpgAgentDraftCardSummary } from './rpgAgentDraft';
import type { RpgCreationPreviewEnvelope } from './rpgCreationPreview';
/**
* RPG Agent 会话层契约。
* 这里承载 session 真相源与会话编排元数据,同时预留 resultPreview 与 supportedActions 两个后续主链字段。
*/
export interface RpgCreationIntentReadiness {
isReady: boolean;
completedKeys: string[];
missingKeys: string[];
}
export interface RpgAgentPendingClarification {
id: string;
label: string;
question: string;
targetKey:
| 'world_hook'
| 'player_premise'
| 'theme_and_tone'
| 'core_conflict'
| 'relationship_seed'
| 'iconic_element';
priority: number;
answer?: string;
}
export type RpgAgentStage =
| 'collecting_intent'
| 'clarifying'
| 'foundation_review'
| 'object_refining'
| 'visual_refining'
| 'long_tail_review'
| 'ready_to_publish'
| 'published'
| 'error';
export type RpgAgentMessageRole = 'user' | 'assistant' | 'system';
export type RpgAgentMessageKind =
| 'chat'
| 'clarification'
| 'summary'
| 'checkpoint'
| 'warning'
| 'action_result';
export interface RpgAgentMessage {
id: string;
role: RpgAgentMessageRole;
kind: RpgAgentMessageKind;
text: string;
createdAt: string;
relatedOperationId?: string | null;
}
export interface RpgAgentQualityFinding {
id: string;
severity: 'info' | 'warning' | 'blocker';
code: string;
targetId?: string | null;
message: string;
}
export interface RpgAgentSessionSnapshot {
sessionId: string;
currentTurn: number;
anchorContent: RpgCreationAnchorContent;
progressPercent: number;
lastAssistantReply: string | null;
stage: RpgAgentStage;
focusCardId: string | null;
creatorIntent: Record<string, unknown> | null;
creatorIntentReadiness: RpgCreationIntentReadiness;
anchorPack: Record<string, unknown> | null;
lockState: Record<string, unknown> | null;
draftProfile: Record<string, unknown> | null;
messages: RpgAgentMessage[];
draftCards: RpgAgentDraftCardSummary[];
pendingClarifications: RpgAgentPendingClarification[];
suggestedActions: RpgAgentSuggestedAction[];
recommendedReplies: string[];
qualityFindings: RpgAgentQualityFinding[];
assetCoverage: RpgAgentAssetCoverageSummary;
/**
* checkpoint 元数据需要进入 session snapshot 主链,
* 这样前端后续才能拿到真实可回滚目标,而不是只能盲发 checkpointId。
*/
checkpoints?: Array<{
checkpointId: string;
createdAt: string;
label: string;
}>;
/**
* 后续由工作包 E 的 action registry 真实填充。
* 当前保持可选,确保主链迁移期间不影响旧 session snapshot。
*/
supportedActions?: RpgAgentSupportedAction[];
/**
* 后续由服务端 preview compiler 输出。
* 当前保持可选,允许前端兼容层继续走 legacy profile。
*/
resultPreview?: RpgCreationPreviewEnvelope | null;
updatedAt: string;
}
export interface CreateRpgAgentSessionRequest {
seedText?: string;
}
export interface CreateRpgAgentSessionResponse {
session: RpgAgentSessionSnapshot;
}
export interface SendRpgAgentMessageRequest {
clientMessageId: string;
text: string;
quickFillRequested?: boolean;
focusCardId?: string | null;
selectedCardIds?: string[];
}
export interface SendRpgAgentMessageResponse extends RpgAgentActionResponse {
operation: RpgAgentOperationRecord;
}
export interface GetRpgAgentCardDetailResponse {
card: RpgAgentDraftCardDetail;
}

View File

@@ -0,0 +1,146 @@
import { describe, expect, test } from 'vitest';
import type { CustomWorldAgentSessionSnapshot } from './customWorldAgentSession';
import type { CustomWorldResultPreviewEnvelope } from './customWorldResultPreview';
import type { CustomWorldWorkSummary } from './customWorldWorkSummary';
import {
createRpgAgentFoundationDraftProfileFixture,
createRpgAgentSupportedActionsFixture,
createRpgAgentSessionFixture,
createRpgCreationAnchorContentFixture,
createRpgCreationPreviewEnvelopeFixture,
createRpgCreationPublishedProfileFixture,
createRpgCreationWorksResponseFixture,
createRpgWorldLibraryEntryFixture,
} from './rpgCreationFixtures';
describe('RPG 创作共享契约 fixture', () => {
test('旧命名兼容分文件可以直接承接新 fixture 的类型消费', () => {
const legacySession: CustomWorldAgentSessionSnapshot =
createRpgAgentSessionFixture();
const legacyPreview: CustomWorldResultPreviewEnvelope =
createRpgCreationPreviewEnvelopeFixture();
const legacyWork: CustomWorldWorkSummary =
createRpgCreationWorksResponseFixture().items[0]!;
expect(legacySession.stage).toBe('ready_to_publish');
expect(legacySession.resultPreview?.source).toBe(legacyPreview.source);
expect(legacyWork.status).toBe('draft');
});
test('anchor fixture 与 foundation draft fixture 保持最小创作真相源对应关系', () => {
const anchors = createRpgCreationAnchorContentFixture();
const draftProfile = createRpgAgentFoundationDraftProfileFixture();
expect(anchors.worldPromise?.hook).toContain('旧航路群岛');
expect(draftProfile.worldHook).toContain('旧航路群岛');
expect(draftProfile.playableNpcs).toHaveLength(1);
expect(draftProfile.storyNpcs).toHaveLength(1);
expect(draftProfile.sceneChapters[0]?.acts[0]?.backgroundImageSrc).toContain(
'/generated-custom-world-scenes/landmark-1/scene-act-1/scene.png',
);
});
test('session fixture 同时暴露 supportedActions 与 resultPreview', () => {
const session = createRpgAgentSessionFixture();
expect(session.sessionId).toBe('rpg-session-fixture');
expect(session.stage).toBe('ready_to_publish');
expect(session.checkpoints?.[0]?.checkpointId).toBe(
'checkpoint-foundation-v1',
);
expect(session.supportedActions?.map((entry) => entry.action)).toEqual(
expect.arrayContaining(['draft_foundation', 'generate_role_assets', 'publish_world']),
);
expect(session.resultPreview?.source).toBe('session_preview');
expect(session.resultPreview?.blockers).toEqual([]);
});
test('preview fixture 保持预览来源、质量结论与 profile 载体三层边界', () => {
const preview = createRpgCreationPreviewEnvelopeFixture();
expect(preview.source).toBe('session_preview');
expect(preview.preview.previewId).toBe('preview-fixture-1');
expect(preview.preview.sessionId).toBe('rpg-session-fixture');
expect(preview.qualityFindings?.[0]).toMatchObject({
severity: 'info',
code: 'scene_asset_ready',
});
});
test('supported actions fixture 明确区分可执行能力矩阵,而不是让前端自行猜测按钮状态', () => {
const supportedActions = createRpgAgentSupportedActionsFixture();
expect(supportedActions).toEqual([
{ action: 'draft_foundation', enabled: true },
{ action: 'generate_role_assets', enabled: true },
{ action: 'publish_world', enabled: true },
]);
});
test('published profile fixture 能稳定承载作品库与结果页所需的封面、场景幕与角色资产字段', () => {
const profile = createRpgCreationPublishedProfileFixture();
expect(profile.id).toBe('rpg-profile-fixture');
expect(profile.playableNpcs).toHaveLength(1);
expect(profile.landmarks).toHaveLength(1);
expect(profile.sceneChapterBlueprints).toHaveLength(1);
expect(
(profile.sceneChapterBlueprints as Array<{ acts?: Array<{ backgroundImageSrc?: string }> }>)[0]
?.acts?.[0]?.backgroundImageSrc,
).toContain('/generated-custom-world-scenes/landmark-1/scene-act-1/scene.png');
});
test('regression: session preview 与 published profile 需要共同保留角色动作资产和分幕背景字段', () => {
const session = createRpgAgentSessionFixture();
const publishedProfile = createRpgCreationPublishedProfileFixture();
const preview = createRpgCreationPreviewEnvelopeFixture();
expect(
((session.draftProfile as { playableNpcs?: Array<{ animationMap?: { run?: { basePath?: string } } }> })
.playableNpcs?.[0]?.animationMap?.run?.basePath ?? ''),
).toContain('/generated-characters/playable-1/animations/run');
expect(
((preview.preview.playableNpcs as Array<{ generatedAnimationSetId?: string }>)[0]
?.generatedAnimationSetId ?? ''),
).toBe('animation-set-playable-1');
expect(
((publishedProfile.sceneChapterBlueprints as Array<{
acts?: Array<{ backgroundAssetId?: string }>;
}>)[0]?.acts?.[0]?.backgroundAssetId ?? ''),
).toBe('scene-asset-runtime');
});
test('works fixture 与 library fixture 对齐同一 published profile', () => {
const works = createRpgCreationWorksResponseFixture();
const libraryEntry = createRpgWorldLibraryEntryFixture();
const publishedWork = works.items.find((entry) => entry.status === 'published');
expect(publishedWork?.profileId).toBe(libraryEntry.profileId);
expect(publishedWork?.title).toBe(libraryEntry.worldName);
expect(publishedWork?.canEnterWorld).toBe(true);
expect(libraryEntry.profile.id).toBe(libraryEntry.profileId);
});
test('regression: works fixture 需要稳定保留草稿与发布态的作品门槛字段', () => {
const works = createRpgCreationWorksResponseFixture();
const draftWork = works.items.find((entry) => entry.status === 'draft');
const publishedWork = works.items.find((entry) => entry.status === 'published');
expect(draftWork).toMatchObject({
stage: 'ready_to_publish',
stageLabel: '准备发布',
canResume: true,
canEnterWorld: false,
roleVisualReadyCount: 2,
roleAnimationReadyCount: 2,
});
expect(publishedWork).toMatchObject({
stage: 'published',
stageLabel: '已发布',
canResume: false,
canEnterWorld: true,
});
});
});

View File

@@ -0,0 +1,714 @@
import type {
CustomWorldLibraryEntry,
CustomWorldProfileRecord,
} from './runtime';
import type { RpgAgentSupportedAction } from './rpgAgentActions';
import type { RpgCreationAnchorContent } from './rpgAgentAnchors';
import type {
RpgAgentAssetCoverageSummary,
RpgAgentDraftCardSummary,
RpgAgentFoundationDraftProfile,
} from './rpgAgentDraft';
import type { RpgAgentSessionSnapshot } from './rpgAgentSession';
import type { RpgCreationPreviewEnvelope } from './rpgCreationPreview';
import type {
ListRpgCreationWorksResponse,
RpgCreationWorkSummary,
} from './rpgCreationWorkSummary';
const RPG_CREATION_FIXTURE_SESSION_ID = 'rpg-session-fixture';
const RPG_CREATION_FIXTURE_PROFILE_ID = 'rpg-profile-fixture';
const RPG_CREATION_FIXTURE_USER_ID = 'fixture-user';
const RPG_CREATION_FIXTURE_UPDATED_AT = '2026-04-21T09:30:00.000Z';
const RPG_CREATION_FIXTURE_PUBLISHED_AT = '2026-04-21T10:00:00.000Z';
function cloneFixture<T>(value: T): T {
return JSON.parse(JSON.stringify(value)) as T;
}
/**
* 共享八锚点 fixture。
* 用于 contract test、session fixture 和 works 集成测试复用同一份创作意图样本。
*/
export function createRpgCreationAnchorContentFixture(): RpgCreationAnchorContent {
return cloneFixture({
worldPromise: {
hook: '被海雾吞没的旧航路群岛',
differentiator: '灯塔与禁航令共同决定谁能活着穿过去。',
desiredExperience: '压抑、悬疑、潮湿',
},
playerFantasy: {
playerRole: '玩家回到群岛调查沉船真相。',
corePursuit: '找出失控航路背后的真相。',
fearOfLoss: '失去最后一个还能对上旧案的人。',
},
themeBoundary: {
toneKeywords: ['压抑', '潮湿', '悬疑'],
aestheticDirectives: ['旧灯塔', '潮雾', '断裂航路'],
forbiddenDirectives: ['不要出现现代枪械'],
},
playerEntryPoint: {
openingIdentity: '被迫返乡的失职守灯人',
openingProblem: '首夜就有陌生船只闯入禁航区。',
entryMotivation: '查清沉船夜里被谁改动了灯册。',
},
coreConflict: {
surfaceConflicts: ['守灯会与航运公会争夺旧航路控制权'],
hiddenCrisis: '沉船夜的航灯与灯册被人动过手脚。',
firstTouchedConflict: '玩家开局就会撞上新的封航命令。',
},
keyRelationships: [
{
pairs: '玩家 / 沈砺',
relationshipType: '旧友兼潜在背叛者',
secretOrCost: '沈砺暗地里在替沉船商盟引路。',
},
],
hiddenLines: {
hiddenTruths: ['沉船夜的真实失误并不是单纯天灾。'],
misdirectionHints: ['所有人都会先把问题推给潮雾本身。'],
revealPacing: '第一章露出痕迹,第二章才让玩家摸到灯册线。',
},
iconicElements: {
iconicMotifs: ['会移动的海雾'],
institutionsOrArtifacts: ['回潮旧灯塔', '封灯令', '旧潮图'],
hardRules: ['禁航信号一旦点亮,任何船都必须退航。'],
},
} satisfies RpgCreationAnchorContent);
}
/**
* 共享 foundation draft fixture。
* 这份样本同时服务 session 草稿、preview 适配回归测试和 works 聚合测试。
*/
export function createRpgAgentFoundationDraftProfileFixture(): RpgAgentFoundationDraftProfile {
return cloneFixture({
name: '潮雾列岛',
subtitle: '旧灯塔与失控航路',
summary: '第一版世界底稿已经整理完成,玩家将从回潮旧灯塔切入沉船旧案。',
tone: '压抑、潮湿、悬疑',
playerGoal: '查清沉船与禁航区异动的真相。',
majorFactions: ['守灯会', '航运公会'],
coreConflicts: ['守灯会与航运公会争夺旧航路控制权'],
playableNpcs: [
{
id: 'playable-1',
name: '沈砺',
title: '旧航路引路人',
role: '关键同行者',
publicIdentity: '最熟悉旧航路的人。',
publicMask: '看上去像可靠旧友。',
currentPressure: '他必须在两股势力间站队。',
hiddenHook: '暗中替沉船商盟引路。',
relationToPlayer: '旧友兼潜在背叛者',
threadIds: ['thread-1'],
summary: '他像旧友,但也像一把始终没收回鞘的刀。',
skills: [
{
id: 'skill-playable-1',
name: '潮行引路',
actionPreviewConfig: {
basePath:
'/generated-characters/playable-1/animations/skills/skill-playable-1',
},
},
],
imageSrc:
'/generated-characters/playable-1/visual/asset-runtime/master.png',
generatedVisualAssetId: 'asset-runtime-playable',
generatedAnimationSetId: 'animation-set-playable-1',
animationMap: {
idle: {
basePath: '/generated-characters/playable-1/animations/idle',
},
run: {
basePath: '/generated-characters/playable-1/animations/run',
},
attack: {
basePath: '/generated-characters/playable-1/animations/attack',
},
},
},
],
storyNpcs: [
{
id: 'story-1',
name: '顾潮音',
title: '守灯会值夜人',
role: '场景关键角色',
publicIdentity: '负责夜间巡灯与封锁。',
publicMask: '对外一直冷静克制。',
currentPressure: '她知道更多禁航区真相。',
hiddenHook: '曾亲眼见过失控海雾吞船。',
relationToPlayer: '最早愿意交换线索的人',
threadIds: ['thread-1'],
summary: '她总像比所有人更早知道海雾会往哪一侧压下来。',
skills: [
{
id: 'skill-story-1',
name: '夜潮灯语',
actionPreviewConfig: {
basePath:
'/generated-characters/story-1/animations/skills/skill-story-1',
},
},
],
imageSrc:
'/generated-characters/story-1/visual/asset-runtime/master.png',
generatedVisualAssetId: 'asset-runtime-story',
generatedAnimationSetId: 'animation-set-story-1',
animationMap: {
run: {
basePath: '/generated-characters/story-1/animations/run',
},
attack: {
basePath: '/generated-characters/story-1/animations/attack',
},
},
},
],
landmarks: [
{
id: 'landmark-1',
name: '回潮旧灯塔',
description: '被海雾啃旧的石塔仍在夜里维持着微弱灯火。',
purpose: '观察雾潮与往来船只',
mood: '潮湿、压抑、风声不止',
importance: '开局核心场景',
secret: '高处潮痕说明海面异常抬升过。',
dangerLevel: 'high',
imageSrc: '/generated-custom-world-scenes/landmark-1/latest-scene.png',
generatedSceneAssetId: 'scene-asset-landmark-1',
characterIds: ['story-1'],
threadIds: ['thread-1'],
summary: '旧灯塔是整片群岛最先看见异动的地方。',
},
],
camp: {
id: 'camp-1',
name: '回潮暂栖所',
description: '能暂时收拢队伍、整理灯册与潮路线索的落脚点。',
mood: '克制、紧绷,但还能暂时收拢局势',
dangerLevel: 'low',
imageSrc: '/custom/camp/huichao.png',
generatedSceneAssetId: 'scene-asset-camp-1',
summary: '玩家能在这里整理情报、回看旧灯册和沉船名单。',
},
themePack: {
id: 'theme-pack:tide',
displayName: '潮雾悬疑',
},
storyGraph: {
visibleThreads: [
{
id: 'thread-visible-1',
title: '封航争夺',
},
],
},
factions: [
{
id: 'faction-1',
name: '守灯会',
title: '守灯会',
subtitle: '把控禁航灯令的人',
publicGoal: '维持封航秩序并压住灯册流出。',
relatedConflict: '想把旧案继续压在禁航记录之下。',
tension: '他们越强调规矩,越像在遮掩灯册。',
playerRelation: '玩家迟早要与他们正面冲突。',
summary: '掌握灯塔与封航令的势力,也是最怕旧案被翻出来的一方。',
},
],
threads: [
{
id: 'thread-1',
title: '沉船旧案',
type: 'main',
conflictType: '真相遮蔽',
conflict: '沉船夜的航灯与灯册被人动过手脚。',
stakes: '真相一旦坐实,群岛秩序会先崩。',
characterIds: ['playable-1', 'story-1'],
landmarkIds: ['landmark-1'],
summary: '玩家会从灯塔高处潮痕一路追到沉船夜的真相。',
},
],
chapters: [
{
id: 'chapter-1',
title: '灯塔回潮',
openingEvent: '禁航区闯入了一艘不该出现的陌生船。',
playerGoal: '先稳住局势,再拿到第一份灯册线索。',
characterIds: ['playable-1', 'story-1'],
landmarkIds: ['landmark-1'],
understandingShift: '玩家会意识到沉船旧案至今仍在操控群岛秩序。',
summary: '第一章聚焦灯塔与封航令,给玩家一条可追的旧案线索。',
},
],
sceneChapters: [
{
id: 'scene-chapter-1',
sceneId: 'landmark-1',
sceneName: '回潮旧灯塔',
title: '灯塔初章',
summary: '围绕灯塔推进的首个场景章节。',
linkedThreadIds: ['thread-1'],
linkedLandmarkIds: ['landmark-1'],
acts: [
{
id: 'scene-act-1',
title: '第一幕',
summary: '先接住回潮灯塔的入口压力。',
stageCoverage: ['opening'],
backgroundImageSrc:
'/generated-custom-world-scenes/landmark-1/scene-act-1/scene.png',
backgroundAssetId: 'scene-asset-runtime',
encounterNpcIds: ['story-1'],
primaryNpcId: 'story-1',
linkedThreadIds: ['thread-1'],
actGoal: '接住首幕入口',
transitionHook: '向第二幕推进。',
advanceRule: 'after_primary_contact',
},
],
},
],
worldHook: '被海雾吞没的旧航路群岛',
playerPremise: '玩家回到群岛调查沉船真相。',
openingSituation: '首夜就有陌生船只闯入禁航区。',
iconicElements: ['会移动的海雾', '回潮旧灯塔'],
sourceAnchorSummary: '海雾、旧灯塔、失控航路。',
} satisfies RpgAgentFoundationDraftProfile);
}
function createRpgAgentDraftCardsFixture(): RpgAgentDraftCardSummary[] {
return cloneFixture([
{
id: 'world-foundation',
kind: 'world',
title: '潮雾列岛',
subtitle: '旧灯塔与失控航路',
summary: '第一版世界底稿已经整理完成。',
status: 'suggested',
linkedIds: ['playable-1', 'story-1', 'landmark-1'],
warningCount: 0,
},
{
id: 'playable-1',
kind: 'character',
title: '沈砺',
subtitle: '旧航路引路人 / 动作已齐',
summary: '最熟悉旧航路的人,也可能是最危险的旧友。',
status: 'suggested',
linkedIds: ['thread-1', 'landmark-1'],
warningCount: 0,
assetStatus: 'complete',
assetStatusLabel: '动作已齐',
},
{
id: 'landmark-1',
kind: 'landmark',
title: '回潮旧灯塔',
subtitle: '观察雾潮与往来船只',
summary: '旧灯塔是整片群岛最先看见异动的地方。',
status: 'suggested',
linkedIds: ['story-1', 'thread-1'],
warningCount: 0,
},
] satisfies RpgAgentDraftCardSummary[]);
}
function createRpgAgentAssetCoverageFixture(): RpgAgentAssetCoverageSummary {
return cloneFixture({
roleAssets: [
{
roleId: 'playable-1',
roleName: '沈砺',
roleKind: 'playable',
priorityTier: 'hero',
portraitPath:
'/generated-characters/playable-1/visual/asset-runtime/master.png',
generatedVisualAssetId: 'asset-runtime-playable',
generatedAnimationSetId: 'animation-set-playable-1',
status: 'complete',
missingAnimations: [],
nextPointCost: 0,
},
{
roleId: 'story-1',
roleName: '顾潮音',
roleKind: 'story',
priorityTier: 'featured',
portraitPath:
'/generated-characters/story-1/visual/asset-runtime/master.png',
generatedVisualAssetId: 'asset-runtime-story',
generatedAnimationSetId: 'animation-set-story-1',
status: 'complete',
missingAnimations: [],
nextPointCost: 0,
},
],
sceneAssets: [
{
sceneId: 'landmark-1',
sceneName: '回潮旧灯塔',
actId: 'scene-act-1',
actTitle: '第一幕',
imageSrc:
'/generated-custom-world-scenes/landmark-1/scene-act-1/scene.png',
assetId: 'scene-asset-runtime',
status: 'ready',
nextPointCost: 0,
},
],
allRoleAssetsReady: true,
allSceneAssetsReady: true,
} satisfies RpgAgentAssetCoverageSummary);
}
/**
* 已发布 profile fixture。
* 用于 preview compiler、works 聚合和 library 元数据解析测试。
*/
export function createRpgCreationPublishedProfileFixture(): CustomWorldProfileRecord {
const draft = createRpgAgentFoundationDraftProfileFixture();
return cloneFixture({
id: RPG_CREATION_FIXTURE_PROFILE_ID,
settingText: draft.worldHook,
name: draft.name,
subtitle: draft.subtitle,
summary: draft.summary,
tone: draft.tone,
playerGoal: draft.playerGoal,
templateWorldType: 'WUXIA',
compatibilityTemplateWorldType: 'WUXIA',
majorFactions: draft.majorFactions,
coreConflicts: draft.coreConflicts,
playableNpcs: draft.playableNpcs.map((role) => ({
id: role.id,
name: role.name,
title: role.title,
role: role.role,
description: role.publicIdentity,
backstory: role.hiddenHook || role.summary,
personality: role.publicMask || role.summary,
motivation: role.currentPressure,
combatStyle: '借地形和潮路换位,先拉扯再压近。',
initialAffinity: 18,
relationshipHooks: [role.relationToPlayer],
tags: ['潮路', '旧案'],
imageSrc: role.imageSrc,
generatedVisualAssetId: role.generatedVisualAssetId,
generatedAnimationSetId: role.generatedAnimationSetId,
animationMap: role.animationMap,
skills: [
{
id: 'skill-playable-1',
name: '潮行引路',
summary: '踩着旧潮阶切线前压,替队伍打开角度。',
style: '机动周旋',
},
],
templateCharacterId: 'archer-hero',
})),
storyNpcs: draft.storyNpcs.map((role) => ({
id: role.id,
name: role.name,
title: role.title,
role: role.role,
description: role.publicIdentity,
backstory: role.hiddenHook || role.summary,
personality: role.publicMask || role.summary,
motivation: role.currentPressure,
combatStyle: '借塔顶视角和风向压制,再用灯火错位扰乱。',
initialAffinity: 8,
relationshipHooks: [role.relationToPlayer],
tags: ['守灯会', '灯塔'],
imageSrc: role.imageSrc,
generatedVisualAssetId: role.generatedVisualAssetId,
skills: [
{
id: 'skill-story-1',
name: '夜潮灯语',
summary: '借灯语与潮声干扰对方判断。',
style: '起手压制',
},
],
})),
camp: {
name: draft.camp?.name,
description: draft.camp?.description,
dangerLevel: draft.camp?.dangerLevel,
imageSrc: draft.camp?.imageSrc,
},
landmarks: draft.landmarks.map((landmark) => ({
id: landmark.id,
name: landmark.name,
description: landmark.description,
dangerLevel: landmark.dangerLevel,
imageSrc: landmark.imageSrc,
sceneNpcIds: landmark.characterIds,
connections: [
{
targetLandmarkId: 'landmark-1',
relativePosition: 'forward',
summary: '沿着旧潮阶继续前压到雾栈尽头。',
},
],
})),
cover: {
sourceType: 'default',
characterRoleIds: ['playable-1'],
},
sceneChapterBlueprints: draft.sceneChapters.map((chapter) => ({
id: chapter.id,
sceneId: chapter.sceneId,
sceneName: chapter.sceneName,
title: chapter.title,
summary: chapter.summary,
acts: chapter.acts.map((act) => ({
id: act.id,
title: act.title,
summary: act.summary,
backgroundImageSrc: act.backgroundImageSrc,
backgroundAssetId: act.backgroundAssetId,
encounterNpcIds: act.encounterNpcIds,
primaryNpcId: act.primaryNpcId,
actGoal: act.actGoal,
transitionHook: act.transitionHook,
})),
})),
themePack: draft.themePack,
storyGraph: draft.storyGraph,
scenarioPackId: 'scenario-pack:tide',
campaignPackId: 'campaign-pack:tide',
generationMode: 'fast',
generationStatus: 'key_only',
updatedAt: RPG_CREATION_FIXTURE_UPDATED_AT,
publishedAt: RPG_CREATION_FIXTURE_PUBLISHED_AT,
} satisfies CustomWorldProfileRecord);
}
export function createRpgCreationPreviewEnvelopeFixture(): RpgCreationPreviewEnvelope {
return cloneFixture({
preview: {
...createRpgCreationPublishedProfileFixture(),
previewId: 'preview-fixture-1',
sessionId: RPG_CREATION_FIXTURE_SESSION_ID,
profileId: RPG_CREATION_FIXTURE_PROFILE_ID,
},
source: 'session_preview',
generatedAt: RPG_CREATION_FIXTURE_UPDATED_AT,
qualityFindings: [
{
id: 'finding-scene-asset-ready',
severity: 'info',
code: 'scene_asset_ready',
targetId: 'scene-act-1',
message: '首幕背景图已经就绪,可直接用于结果页预览。',
},
],
blockers: [],
publishReady: true,
canEnterWorld: false,
} satisfies RpgCreationPreviewEnvelope);
}
export function createRpgAgentSupportedActionsFixture(): RpgAgentSupportedAction[] {
return cloneFixture([
{
action: 'draft_foundation',
enabled: true,
},
{
action: 'generate_role_assets',
enabled: true,
},
{
action: 'publish_world',
enabled: true,
},
] satisfies RpgAgentSupportedAction[]);
}
/**
* 共享 session snapshot fixture。
* 默认模拟“底稿、预览、资产都已准备好”的 ready_to_publish 状态。
*/
export function createRpgAgentSessionFixture(): RpgAgentSessionSnapshot {
const draftProfile = createRpgAgentFoundationDraftProfileFixture();
return cloneFixture({
sessionId: RPG_CREATION_FIXTURE_SESSION_ID,
currentTurn: 6,
anchorContent: createRpgCreationAnchorContentFixture(),
progressPercent: 100,
lastAssistantReply: '八锚点与底稿都已经齐备,可以进入结果页收口。',
stage: 'ready_to_publish',
focusCardId: null,
creatorIntent: {
sourceMode: 'card',
rawSettingText: draftProfile.worldHook,
worldHook: draftProfile.worldHook,
playerPremise: draftProfile.playerPremise,
themeKeywords: ['海雾', '旧航路'],
toneDirectives: ['压抑', '悬疑'],
openingSituation: draftProfile.openingSituation,
coreConflicts: draftProfile.coreConflicts,
keyFactions: ['守灯会'],
keyCharacters: ['沈砺', '顾潮音'],
keyLandmarks: ['回潮旧灯塔'],
iconicElements: draftProfile.iconicElements,
forbiddenDirectives: ['不要出现现代枪械'],
},
creatorIntentReadiness: {
isReady: true,
completedKeys: [
'world_hook',
'player_premise',
'theme_and_tone',
'core_conflict',
'relationship_seed',
'iconic_element',
],
missingKeys: [],
},
anchorPack: {
summary: draftProfile.sourceAnchorSummary,
},
lockState: {
lockedCardIds: ['world-foundation'],
},
draftProfile,
messages: [
{
id: 'message-1',
role: 'assistant',
kind: 'summary',
text: '世界底稿已整理完成,建议进入结果页确认资产与发布门槛。',
createdAt: RPG_CREATION_FIXTURE_UPDATED_AT,
relatedOperationId: null,
},
],
draftCards: createRpgAgentDraftCardsFixture(),
pendingClarifications: [],
suggestedActions: [
{
id: 'action-publish',
type: 'publish_world',
label: '发布世界',
},
],
recommendedReplies: ['先看结果页', '继续精修角色关系'],
qualityFindings: [
{
id: 'finding-scene-asset-ready',
severity: 'info',
code: 'scene_asset_ready',
targetId: 'scene-act-1',
message: '首幕背景图已经就绪,可直接用于结果页预览。',
},
],
assetCoverage: createRpgAgentAssetCoverageFixture(),
checkpoints: [
{
checkpointId: 'checkpoint-foundation-v1',
createdAt: RPG_CREATION_FIXTURE_UPDATED_AT,
label: '世界底稿 V1',
},
],
supportedActions: createRpgAgentSupportedActionsFixture(),
resultPreview: createRpgCreationPreviewEnvelopeFixture(),
updatedAt: RPG_CREATION_FIXTURE_UPDATED_AT,
} satisfies RpgAgentSessionSnapshot);
}
export function createRpgWorldLibraryEntryFixture(): CustomWorldLibraryEntry<CustomWorldProfileRecord> {
const profile = createRpgCreationPublishedProfileFixture();
return cloneFixture({
ownerUserId: RPG_CREATION_FIXTURE_USER_ID,
profileId: RPG_CREATION_FIXTURE_PROFILE_ID,
profile,
visibility: 'published',
publishedAt: RPG_CREATION_FIXTURE_PUBLISHED_AT,
updatedAt: RPG_CREATION_FIXTURE_UPDATED_AT,
authorDisplayName: '测试玩家',
worldName: String(profile.name ?? '潮雾列岛'),
subtitle: String(profile.subtitle ?? '旧灯塔与失控航路'),
summaryText: String(profile.summary ?? '第一版世界底稿已经整理完成。'),
coverImageSrc:
'/generated-custom-world-scenes/landmark-1/scene-act-1/scene.png',
themeMode: 'tide',
playableNpcCount: Array.isArray(profile.playableNpcs)
? profile.playableNpcs.length
: 0,
landmarkCount: Array.isArray(profile.landmarks)
? profile.landmarks.length
: 0,
} satisfies CustomWorldLibraryEntry<CustomWorldProfileRecord>);
}
export function createRpgCreationWorksResponseFixture(): ListRpgCreationWorksResponse {
return cloneFixture({
items: [
{
workId: `draft:${RPG_CREATION_FIXTURE_SESSION_ID}`,
sourceType: 'agent_session',
status: 'draft',
title: '潮雾列岛',
subtitle: '旧灯塔与失控航路',
summary: '第一版世界底稿已经整理完成,玩家将从回潮旧灯塔切入沉船旧案。',
coverImageSrc: '/custom/camp/huichao.png',
coverRenderMode: 'scene_with_roles',
coverCharacterImageSrcs: [
'/generated-characters/playable-1/visual/asset-runtime/master.png',
],
updatedAt: RPG_CREATION_FIXTURE_UPDATED_AT,
publishedAt: null,
stage: 'ready_to_publish',
stageLabel: '准备发布',
playableNpcCount: 2,
landmarkCount: 1,
roleVisualReadyCount: 2,
roleAnimationReadyCount: 2,
roleAssetSummaryLabel: '沈砺 · 动作已就绪',
sessionId: RPG_CREATION_FIXTURE_SESSION_ID,
profileId: null,
canResume: true,
canEnterWorld: false,
blockerCount: 0,
publishReady: true,
},
{
workId: `published:${RPG_CREATION_FIXTURE_PROFILE_ID}`,
sourceType: 'published_profile',
status: 'published',
title: '潮雾列岛',
subtitle: '旧灯塔与失控航路',
summary: '第一版世界底稿已经整理完成,玩家将从回潮旧灯塔切入沉船旧案。',
coverImageSrc:
'/generated-custom-world-scenes/landmark-1/scene-act-1/scene.png',
coverRenderMode: 'scene_with_roles',
coverCharacterImageSrcs: [
'/generated-characters/playable-1/visual/asset-runtime/master.png',
],
updatedAt: RPG_CREATION_FIXTURE_UPDATED_AT,
publishedAt: RPG_CREATION_FIXTURE_PUBLISHED_AT,
stage: 'published',
stageLabel: '已发布',
playableNpcCount: 1,
landmarkCount: 1,
roleVisualReadyCount: 1,
roleAnimationReadyCount: 1,
roleAssetSummaryLabel: '动作已就绪 1',
sessionId: null,
profileId: RPG_CREATION_FIXTURE_PROFILE_ID,
canResume: false,
canEnterWorld: true,
blockerCount: 0,
publishReady: true,
},
] satisfies RpgCreationWorkSummary[],
} satisfies ListRpgCreationWorksResponse);
}

View File

@@ -0,0 +1,40 @@
import type { CustomWorldProfileRecord } from './runtime';
/**
* 结果页预览契约。
* 当前 preview 仍以兼容 profile 作为承载体,但已经把来源、阻断项和质量结论从 session 草稿里显式剥离出来。
*/
export type RpgCreationPreviewSource =
| 'session_preview'
| 'published_profile';
export interface RpgCreationPreviewFinding {
id: string;
severity: 'info' | 'warning' | 'blocker';
code: string;
targetId?: string | null;
message: string;
}
export interface RpgCreationPreviewBlocker {
id: string;
code: string;
message: string;
}
export type RpgCreationPreview = CustomWorldProfileRecord & {
previewId?: string;
sessionId?: string | null;
profileId?: string | null;
};
export interface RpgCreationPreviewEnvelope {
preview: RpgCreationPreview;
source: RpgCreationPreviewSource;
generatedAt?: string;
qualityFindings?: RpgCreationPreviewFinding[];
blockers?: RpgCreationPreviewBlocker[];
publishReady?: boolean;
canEnterWorld?: boolean;
}

View File

@@ -0,0 +1,38 @@
/**
* RPG 创作作品卡读模型契约。
* works 列表只暴露继续创作与进入世界判断所需的稳定字段。
*/
export type RpgCreationWorkStatus = 'draft' | 'published';
export type RpgCreationWorkSource = 'agent_session' | 'published_profile';
export interface RpgCreationWorkSummary {
workId: string;
sourceType: RpgCreationWorkSource;
status: RpgCreationWorkStatus;
title: string;
subtitle: string;
summary: string;
coverImageSrc?: string | null;
coverRenderMode?: 'image' | 'scene_with_roles';
coverCharacterImageSrcs?: string[];
updatedAt: string;
publishedAt?: string | null;
stage?: string | null;
stageLabel?: string | null;
playableNpcCount: number;
landmarkCount: number;
roleVisualReadyCount?: number;
roleAnimationReadyCount?: number;
roleAssetSummaryLabel?: string | null;
sessionId?: string | null;
profileId?: string | null;
canResume: boolean;
canEnterWorld: boolean;
blockerCount?: number;
publishReady?: boolean;
}
export interface ListRpgCreationWorksResponse {
items: RpgCreationWorkSummary[];
}

View File

@@ -0,0 +1,184 @@
/**
* RPG 运行时聊天相关共享契约。
* 将角色聊天、NPC 对话与轻量 story 请求载荷从旧 story.ts 中独立出来。
*/
import type { JsonObject } from './common';
export type NpcChatTurnLimitReason = 'negative_affinity';
export type NpcChatTurnClosingMode = 'free' | 'foreshadow_close';
export type NpcChatTurnDirective = {
sceneActId?: string | null;
turnLimit?: number | null;
remainingTurns?: number | null;
limitReason?: NpcChatTurnLimitReason | null;
closingMode?: NpcChatTurnClosingMode | null;
forceExitAfterTurn?: boolean;
};
export type NpcChatTurnCompletionDirective = {
turnLimit?: number | null;
remainingTurns?: number | null;
forceExit?: boolean;
closingMode?: NpcChatTurnClosingMode;
};
export type CharacterChatReplyRequest<
TCharacter = unknown,
TStoryMoment = unknown,
TContext = unknown,
TConversationTurn = unknown,
TTargetStatus = unknown,
> = {
worldType: string;
playerCharacter: TCharacter;
targetCharacter: TCharacter;
storyHistory: TStoryMoment[];
context: TContext;
conversationHistory: TConversationTurn[];
conversationSummary: string;
playerMessage: string;
targetStatus: TTargetStatus;
};
export type CharacterChatSuggestionsRequest<
TCharacter = unknown,
TStoryMoment = unknown,
TContext = unknown,
TConversationTurn = unknown,
TTargetStatus = unknown,
> = {
worldType: string;
playerCharacter: TCharacter;
targetCharacter: TCharacter;
storyHistory: TStoryMoment[];
context: TContext;
conversationHistory: TConversationTurn[];
conversationSummary: string;
targetStatus: TTargetStatus;
};
export type CharacterChatSummaryRequest<
TCharacter = unknown,
TStoryMoment = unknown,
TContext = unknown,
TConversationTurn = unknown,
TTargetStatus = unknown,
> = {
worldType: string;
playerCharacter: TCharacter;
targetCharacter: TCharacter;
storyHistory: TStoryMoment[];
context: TContext;
conversationHistory: TConversationTurn[];
previousSummary: string;
targetStatus: TTargetStatus;
};
export type NpcChatDialogueRequest<
TCharacter = unknown,
TEncounter = unknown,
TMonster = unknown,
TStoryMoment = unknown,
TContext = unknown,
> = {
worldType: string;
character: TCharacter;
encounter: TEncounter;
monsters: TMonster[];
history: TStoryMoment[];
context: TContext;
topic: string;
resultSummary: string;
npcInitiatesConversation?: boolean;
};
export type NpcChatTurnRequest<
TCharacter = unknown,
TEncounter = unknown,
TMonster = unknown,
TStoryMoment = unknown,
TContext = unknown,
TConversationTurn = unknown,
TCombatContext = unknown,
TNpcState = unknown,
TQuestOfferState = unknown,
TQuestOfferEncounter = unknown,
TChatDirective = NpcChatTurnDirective,
> = {
worldType: string;
character?: TCharacter;
player?: TCharacter;
encounter: TEncounter;
monsters: TMonster[];
history: TStoryMoment[];
context: TContext;
conversationHistory?: TConversationTurn[];
dialogue?: TConversationTurn[];
combatContext?: TCombatContext | null;
playerMessage: string;
npcState: TNpcState;
npcInitiatesConversation?: boolean;
questOfferContext?: {
state: TQuestOfferState;
encounter: TQuestOfferEncounter;
turnCount: number;
} | null;
chatDirective?: TChatDirective | null;
};
export type NpcChatPendingQuestOffer<TQuest = unknown> = {
quest: TQuest;
introText?: string;
};
export type NpcChatTurnResult<TQuest = unknown> = {
npcReply: string;
affinityDelta: number;
affinityText: string;
suggestions: string[];
pendingQuestOffer?: NpcChatPendingQuestOffer<TQuest> | null;
chatDirective?: NpcChatTurnCompletionDirective | null;
};
export type NpcRecruitDialogueRequest<
TCharacter = unknown,
TEncounter = unknown,
TMonster = unknown,
TStoryMoment = unknown,
TContext = unknown,
> = {
worldType: string;
character: TCharacter;
encounter: TEncounter;
monsters: TMonster[];
history: TStoryMoment[];
context: TContext;
invitationText: string;
recruitSummary: string;
};
export type StoryRequestOptionsPayload = {
availableOptions?: JsonObject[];
optionCatalog?: JsonObject[];
};
export type StoryRequestPayload<TWorldType extends string = string> = {
worldType: TWorldType;
character: JsonObject;
monsters?: JsonObject[];
history?: JsonObject[];
choice?: string;
context: JsonObject;
requestOptions?: StoryRequestOptionsPayload;
};
export type PlainTextPromptRequest = {
systemPrompt: string;
userPrompt: string;
};
export type PlainTextResponse = {
text: string;
};

View File

@@ -0,0 +1,51 @@
import { describe, expect, test } from 'vitest';
import type { CharacterChatReplyRequest } from './rpgRuntimeChat';
import { QUEST_NARRATIVE_TYPES } from './rpgRuntimeQuestAssist';
import {
SERVER_RUNTIME_FUNCTION_IDS,
TASK5_RUNTIME_OPTION_SCOPES,
TASK6_RUNTIME_FUNCTION_IDS,
type RuntimeStoryActionRequest,
} from './rpgRuntimeStoryAction';
import type { RuntimeStoryStateRequest } from './rpgRuntimeStoryState';
describe('RPG runtime shared contracts', () => {
test('拆分后的 runtime story action 契约继续导出常量与类型', () => {
expect(SERVER_RUNTIME_FUNCTION_IDS).toContain('npc_chat');
expect(TASK6_RUNTIME_FUNCTION_IDS).toContain('npc_trade');
expect(TASK5_RUNTIME_OPTION_SCOPES).toEqual(['story', 'combat', 'npc']);
const request: RuntimeStoryActionRequest = {
sessionId: 'runtime-session-1',
action: {
type: 'story_choice',
functionId: 'npc_chat',
},
};
expect(request.action.functionId).toBe('npc_chat');
});
test('拆分后的 chat 与 quest assist 契约继续导出运行时类型', () => {
const payload: CharacterChatReplyRequest = {
worldType: 'WUXIA',
playerCharacter: {},
targetCharacter: {},
storyHistory: [],
context: {},
conversationHistory: [],
conversationSummary: '测试摘要',
playerMessage: '近况如何?',
targetStatus: {},
};
const stateRequest: RuntimeStoryStateRequest = {
sessionId: 'runtime-session-2',
};
expect(payload.playerMessage).toBe('近况如何?');
expect(stateRequest.sessionId).toBe('runtime-session-2');
expect(QUEST_NARRATIVE_TYPES).toContain('relationship');
});
});

View File

@@ -0,0 +1,83 @@
/**
* RPG 运行时任务辅助与道具意图共享契约。
* 该文件只承载 quest / runtime item 辅助类型,不混入 runtime story 主状态。
*/
import type { JsonObject } from './common';
export const QUEST_NARRATIVE_TYPES = [
'bounty',
'escort',
'investigation',
'retrieval',
'relationship',
'trial',
] as const;
export type SharedQuestNarrativeType = (typeof QUEST_NARRATIVE_TYPES)[number];
export const QUEST_OBJECTIVE_KINDS = [
'defeat_hostile_npc',
'inspect_treasure',
'spar_with_npc',
'talk_to_npc',
'reach_scene',
'deliver_item',
] as const;
export type SharedQuestObjectiveKind = (typeof QUEST_OBJECTIVE_KINDS)[number];
export const QUEST_URGENCY_LEVELS = ['low', 'medium', 'high'] as const;
export type SharedQuestUrgency = (typeof QUEST_URGENCY_LEVELS)[number];
export const QUEST_INTIMACY_LEVELS = [
'transactional',
'cooperative',
'trust_based',
] as const;
export type SharedQuestIntimacy = (typeof QUEST_INTIMACY_LEVELS)[number];
export const QUEST_REWARD_THEMES = [
'currency',
'resource',
'relationship',
'intel',
'rare_item',
] as const;
export type SharedQuestRewardTheme = (typeof QUEST_REWARD_THEMES)[number];
export const RUNTIME_ITEM_FUNCTIONAL_BIAS_VALUES = [
'heal',
'mana',
'cooldown',
'guard',
'damage',
] as const;
export type SharedRuntimeItemFunctionalBias =
(typeof RUNTIME_ITEM_FUNCTIONAL_BIAS_VALUES)[number];
export const RUNTIME_ITEM_TONE_VALUES = [
'grim',
'mysterious',
'martial',
'ritual',
'survival',
] as const;
export type SharedRuntimeItemTone = (typeof RUNTIME_ITEM_TONE_VALUES)[number];
export type RuntimeItemIntentRequest<
TContext = JsonObject,
TPlan = JsonObject,
> = {
context: TContext;
plans: TPlan[];
};
export type RuntimeItemIntentResponse<TIntent = JsonObject> = {
intents: TIntent[];
};
export type QuestGenerationRequest<
TState = JsonObject,
TEncounter = JsonObject,
> = {
state: TState;
encounter: TEncounter;
};

View File

@@ -0,0 +1,136 @@
/**
* RPG runtime story 动作层共享契约。
* 将 function id、动作请求与交互元数据从旧 story.ts 中单独收口。
*/
import type { JsonObject } from './common';
export type RuntimeAction<
TType extends string = string,
TPayload = JsonObject,
> = {
type: TType;
functionId?: string;
targetId?: string;
payload?: TPayload;
};
export type RuntimeActionRequest<
TAction extends RuntimeAction = RuntimeAction,
> = {
sessionId: string;
clientVersion?: number;
action: TAction;
};
export type RuntimeActionResponse<
TViewModel = JsonObject,
TPresentation = JsonObject,
TPatch = JsonObject,
> = {
sessionId: string;
serverVersion: number;
viewModel: TViewModel;
presentation: TPresentation;
patches: TPatch[];
};
export const TASK5_RUNTIME_FUNCTION_IDS = [
'story_continue_adventure',
'story_opening_camp_dialogue',
'camp_travel_home_scene',
'idle_call_out',
'idle_explore_forward',
'idle_observe_signs',
'idle_rest_focus',
'idle_travel_next_scene',
'battle_attack_basic',
'battle_use_skill',
'battle_all_in_crush',
'battle_escape_breakout',
'battle_feint_step',
'battle_finisher_window',
'battle_guard_break',
'battle_probe_pressure',
'battle_recover_breath',
'npc_chat',
'npc_fight',
'npc_help',
'npc_leave',
'npc_preview_talk',
'npc_recruit',
'npc_spar',
] as const;
export type Task5RuntimeFunctionId =
(typeof TASK5_RUNTIME_FUNCTION_IDS)[number];
export const TASK6_RUNTIME_FUNCTION_IDS = [
'equipment_equip',
'equipment_unequip',
'forge_craft',
'forge_dismantle',
'forge_reforge',
'inventory_use',
'npc_gift',
'npc_chat_quest_offer_abandon',
'npc_chat_quest_offer_replace',
'npc_chat_quest_offer_view',
'npc_quest_accept',
'npc_quest_turn_in',
'npc_trade',
'treasure_inspect',
'treasure_leave',
'treasure_secure',
] as const;
export type Task6RuntimeFunctionId =
(typeof TASK6_RUNTIME_FUNCTION_IDS)[number];
export const SERVER_RUNTIME_FUNCTION_IDS = [
...TASK5_RUNTIME_FUNCTION_IDS,
...TASK6_RUNTIME_FUNCTION_IDS,
] as const;
export type ServerRuntimeFunctionId =
(typeof SERVER_RUNTIME_FUNCTION_IDS)[number];
export const TASK5_RUNTIME_OPTION_SCOPES = ['story', 'combat', 'npc'] as const;
export type Task5RuntimeOptionScope =
(typeof TASK5_RUNTIME_OPTION_SCOPES)[number];
export type RuntimeStoryChoicePayload = JsonObject & {
optionText?: string;
note?: string;
releaseNpcId?: string;
preludeText?: string;
};
export type RuntimeStoryOptionInteraction =
| {
kind: 'npc';
npcId: string;
action:
| 'chat'
| 'help'
| 'fight'
| 'leave'
| 'quest_offer_abandon'
| 'quest_offer_replace'
| 'quest_offer_view'
| 'recruit'
| 'spar'
| 'trade'
| 'gift'
| 'quest_accept'
| 'quest_turn_in';
questId?: string;
}
| {
kind: 'treasure';
action: 'inspect' | 'leave' | 'secure';
};
export type RuntimeStoryChoiceAction = RuntimeAction<
'story_choice',
RuntimeStoryChoicePayload
> & {
functionId: string;
targetId?: string;
};

View File

@@ -0,0 +1,146 @@
/**
* RPG runtime story 状态与响应共享契约。
* 该文件只负责 view model、presentation、patch 与 snapshot 回包结构。
*/
import type { JsonObject } from './common';
import type { SavedGameSnapshot, SavedGameSnapshotInput } from './runtime';
import type {
RuntimeActionRequest,
RuntimeActionResponse,
RuntimeStoryChoiceAction,
RuntimeStoryChoicePayload,
RuntimeStoryOptionInteraction,
Task5RuntimeOptionScope,
} from './rpgRuntimeStoryAction';
export type RuntimeStoryOptionView = {
functionId: string;
actionText: string;
detailText?: string;
scope: Task5RuntimeOptionScope;
interaction?: RuntimeStoryOptionInteraction;
payload?: RuntimeStoryChoicePayload;
disabled?: boolean;
reason?: string;
};
export type RuntimeStoryPlayerViewModel = {
hp: number;
maxHp: number;
mana: number;
maxMana: number;
};
export type RuntimeStoryCompanionViewModel = {
npcId: string;
characterId?: string;
joinedAtAffinity: number;
};
export type RuntimeStoryEncounterViewModel = {
id: string;
kind: 'npc' | 'treasure';
npcName: string;
hostile: boolean;
affinity?: number;
recruited?: boolean;
interactionActive: boolean;
battleMode?: 'fight' | 'spar' | null;
};
export type RuntimeStoryStatusViewModel = {
inBattle: boolean;
npcInteractionActive: boolean;
currentNpcBattleMode: 'fight' | 'spar' | null;
currentNpcBattleOutcome: 'fight_victory' | 'spar_complete' | null;
};
export type RuntimeBattlePresentation = {
targetId?: string;
targetName?: string;
damageDealt?: number;
damageTaken?: number;
outcome?: 'ongoing' | 'victory' | 'spar_complete' | 'escaped';
};
export type RuntimeStoryViewModel = {
player: RuntimeStoryPlayerViewModel;
encounter: RuntimeStoryEncounterViewModel | null;
companions: RuntimeStoryCompanionViewModel[];
availableOptions: RuntimeStoryOptionView[];
status: RuntimeStoryStatusViewModel;
};
export type RuntimeStoryPresentation = {
actionText: string;
resultText: string;
storyText: string;
options: RuntimeStoryOptionView[];
toast?: string | null;
battle?: RuntimeBattlePresentation | null;
};
export type RuntimeStoryPatch =
| {
type: 'story_history_append';
actionText: string;
resultText: string;
}
| {
type: 'npc_affinity_changed';
npcId: string;
previousAffinity: number;
nextAffinity: number;
}
| {
type: 'battle_resolved';
functionId: string;
targetId?: string;
damageDealt?: number;
damageTaken?: number;
outcome: 'ongoing' | 'victory' | 'spar_complete' | 'escaped';
}
| {
type: 'status_changed';
inBattle: boolean;
npcInteractionActive: boolean;
currentNpcBattleMode: 'fight' | 'spar' | null;
currentNpcBattleOutcome: 'fight_victory' | 'spar_complete' | null;
}
| {
type: 'encounter_changed';
encounterId: string | null;
};
export type RuntimeStoryActionRequest =
RuntimeActionRequest<RuntimeStoryChoiceAction> & {
snapshot?: SavedGameSnapshotInput;
};
export type RuntimeStoryStateRequest<
TSnapshotGameState = JsonObject,
TSnapshotCurrentStory = JsonObject,
> = {
sessionId: string;
clientVersion?: number;
snapshot?: SavedGameSnapshotInput<
TSnapshotGameState,
string,
TSnapshotCurrentStory
>;
};
export type RuntimeStoryActionResponse<
TSnapshotGameState = JsonObject,
TSnapshotCurrentStory = JsonObject,
> = RuntimeActionResponse<
RuntimeStoryViewModel,
RuntimeStoryPresentation,
RuntimeStoryPatch
> & {
snapshot: SavedGameSnapshot<
TSnapshotGameState,
string,
TSnapshotCurrentStory
>;
};

View File

@@ -1,499 +0,0 @@
import type { JsonObject } from './common';
import type { SavedGameSnapshot } from './runtime';
export const QUEST_NARRATIVE_TYPES = [
'bounty',
'escort',
'investigation',
'retrieval',
'relationship',
'trial',
] as const;
export type SharedQuestNarrativeType = (typeof QUEST_NARRATIVE_TYPES)[number];
export const QUEST_OBJECTIVE_KINDS = [
'defeat_hostile_npc',
'inspect_treasure',
'spar_with_npc',
'talk_to_npc',
'reach_scene',
'deliver_item',
] as const;
export type SharedQuestObjectiveKind = (typeof QUEST_OBJECTIVE_KINDS)[number];
export const QUEST_URGENCY_LEVELS = ['low', 'medium', 'high'] as const;
export type SharedQuestUrgency = (typeof QUEST_URGENCY_LEVELS)[number];
export const QUEST_INTIMACY_LEVELS = [
'transactional',
'cooperative',
'trust_based',
] as const;
export type SharedQuestIntimacy = (typeof QUEST_INTIMACY_LEVELS)[number];
export const QUEST_REWARD_THEMES = [
'currency',
'resource',
'relationship',
'intel',
'rare_item',
] as const;
export type SharedQuestRewardTheme = (typeof QUEST_REWARD_THEMES)[number];
export const RUNTIME_ITEM_FUNCTIONAL_BIAS_VALUES = [
'heal',
'mana',
'cooldown',
'guard',
'damage',
] as const;
export type SharedRuntimeItemFunctionalBias =
(typeof RUNTIME_ITEM_FUNCTIONAL_BIAS_VALUES)[number];
export const RUNTIME_ITEM_TONE_VALUES = [
'grim',
'mysterious',
'martial',
'ritual',
'survival',
] as const;
export type SharedRuntimeItemTone = (typeof RUNTIME_ITEM_TONE_VALUES)[number];
export type StoryRequestOptionsPayload = {
availableOptions?: JsonObject[];
optionCatalog?: JsonObject[];
};
export type StoryRequestPayload<TWorldType extends string = string> = {
worldType: TWorldType;
character: JsonObject;
monsters?: JsonObject[];
history?: JsonObject[];
choice?: string;
context: JsonObject;
requestOptions?: StoryRequestOptionsPayload;
};
export type PlainTextPromptRequest = {
systemPrompt: string;
userPrompt: string;
};
export type PlainTextResponse = {
text: string;
};
export type NpcChatTurnLimitReason = 'negative_affinity';
export type NpcChatTurnClosingMode = 'free' | 'foreshadow_close';
export type NpcChatTurnDirective = {
sceneActId?: string | null;
turnLimit?: number | null;
remainingTurns?: number | null;
limitReason?: NpcChatTurnLimitReason | null;
closingMode?: NpcChatTurnClosingMode | null;
forceExitAfterTurn?: boolean;
};
export type NpcChatTurnCompletionDirective = {
turnLimit?: number | null;
remainingTurns?: number | null;
forceExit?: boolean;
closingMode?: NpcChatTurnClosingMode;
};
export type CharacterChatReplyRequest<
TCharacter = unknown,
TStoryMoment = unknown,
TContext = unknown,
TConversationTurn = unknown,
TTargetStatus = unknown,
> = {
worldType: string;
playerCharacter: TCharacter;
targetCharacter: TCharacter;
storyHistory: TStoryMoment[];
context: TContext;
conversationHistory: TConversationTurn[];
conversationSummary: string;
playerMessage: string;
targetStatus: TTargetStatus;
};
export type CharacterChatSuggestionsRequest<
TCharacter = unknown,
TStoryMoment = unknown,
TContext = unknown,
TConversationTurn = unknown,
TTargetStatus = unknown,
> = {
worldType: string;
playerCharacter: TCharacter;
targetCharacter: TCharacter;
storyHistory: TStoryMoment[];
context: TContext;
conversationHistory: TConversationTurn[];
conversationSummary: string;
targetStatus: TTargetStatus;
};
export type CharacterChatSummaryRequest<
TCharacter = unknown,
TStoryMoment = unknown,
TContext = unknown,
TConversationTurn = unknown,
TTargetStatus = unknown,
> = {
worldType: string;
playerCharacter: TCharacter;
targetCharacter: TCharacter;
storyHistory: TStoryMoment[];
context: TContext;
conversationHistory: TConversationTurn[];
previousSummary: string;
targetStatus: TTargetStatus;
};
export type NpcChatDialogueRequest<
TCharacter = unknown,
TEncounter = unknown,
TMonster = unknown,
TStoryMoment = unknown,
TContext = unknown,
> = {
worldType: string;
character: TCharacter;
encounter: TEncounter;
monsters: TMonster[];
history: TStoryMoment[];
context: TContext;
topic: string;
resultSummary: string;
npcInitiatesConversation?: boolean;
};
export type NpcChatTurnRequest<
TCharacter = unknown,
TEncounter = unknown,
TMonster = unknown,
TStoryMoment = unknown,
TContext = unknown,
TConversationTurn = unknown,
TCombatContext = unknown,
TNpcState = unknown,
TQuestOfferState = unknown,
TQuestOfferEncounter = unknown,
TChatDirective = NpcChatTurnDirective,
> = {
worldType: string;
character?: TCharacter;
player?: TCharacter;
encounter: TEncounter;
monsters: TMonster[];
history: TStoryMoment[];
context: TContext;
conversationHistory?: TConversationTurn[];
dialogue?: TConversationTurn[];
combatContext?: TCombatContext | null;
playerMessage: string;
npcState: TNpcState;
npcInitiatesConversation?: boolean;
questOfferContext?: {
state: TQuestOfferState;
encounter: TQuestOfferEncounter;
turnCount: number;
} | null;
chatDirective?: TChatDirective | null;
};
export type NpcChatPendingQuestOffer<TQuest = unknown> = {
quest: TQuest;
introText?: string;
};
export type NpcChatTurnResult<TQuest = unknown> = {
npcReply: string;
affinityDelta: number;
affinityText: string;
suggestions: string[];
pendingQuestOffer?: NpcChatPendingQuestOffer<TQuest> | null;
chatDirective?: NpcChatTurnCompletionDirective | null;
};
export type NpcRecruitDialogueRequest<
TCharacter = unknown,
TEncounter = unknown,
TMonster = unknown,
TStoryMoment = unknown,
TContext = unknown,
> = {
worldType: string;
character: TCharacter;
encounter: TEncounter;
monsters: TMonster[];
history: TStoryMoment[];
context: TContext;
invitationText: string;
recruitSummary: string;
};
export type RuntimeItemIntentRequest<
TContext = JsonObject,
TPlan = JsonObject,
> = {
context: TContext;
plans: TPlan[];
};
export type RuntimeItemIntentResponse<TIntent = JsonObject> = {
intents: TIntent[];
};
export type QuestGenerationRequest<
TState = JsonObject,
TEncounter = JsonObject,
> = {
state: TState;
encounter: TEncounter;
};
export type RuntimeAction<
TType extends string = string,
TPayload = JsonObject,
> = {
type: TType;
functionId?: string;
targetId?: string;
payload?: TPayload;
};
export type RuntimeActionRequest<
TAction extends RuntimeAction = RuntimeAction,
> = {
sessionId: string;
clientVersion?: number;
action: TAction;
};
export type RuntimeActionResponse<
TViewModel = JsonObject,
TPresentation = JsonObject,
TPatch = JsonObject,
> = {
sessionId: string;
serverVersion: number;
viewModel: TViewModel;
presentation: TPresentation;
patches: TPatch[];
};
export const TASK5_RUNTIME_FUNCTION_IDS = [
'story_continue_adventure',
'story_opening_camp_dialogue',
'camp_travel_home_scene',
'idle_call_out',
'idle_explore_forward',
'idle_observe_signs',
'idle_rest_focus',
'idle_travel_next_scene',
'battle_attack_basic',
'battle_use_skill',
'battle_all_in_crush',
'battle_escape_breakout',
'battle_feint_step',
'battle_finisher_window',
'battle_guard_break',
'battle_probe_pressure',
'battle_recover_breath',
'npc_chat',
'npc_fight',
'npc_help',
'npc_leave',
'npc_preview_talk',
'npc_recruit',
'npc_spar',
] as const;
export type Task5RuntimeFunctionId =
(typeof TASK5_RUNTIME_FUNCTION_IDS)[number];
export const TASK6_RUNTIME_FUNCTION_IDS = [
'equipment_equip',
'equipment_unequip',
'forge_craft',
'forge_dismantle',
'forge_reforge',
'inventory_use',
'npc_gift',
'npc_quest_accept',
'npc_quest_turn_in',
'npc_trade',
'treasure_inspect',
'treasure_leave',
'treasure_secure',
] as const;
export type Task6RuntimeFunctionId =
(typeof TASK6_RUNTIME_FUNCTION_IDS)[number];
export const SERVER_RUNTIME_FUNCTION_IDS = [
...TASK5_RUNTIME_FUNCTION_IDS,
...TASK6_RUNTIME_FUNCTION_IDS,
] as const;
export type ServerRuntimeFunctionId =
(typeof SERVER_RUNTIME_FUNCTION_IDS)[number];
export const TASK5_RUNTIME_OPTION_SCOPES = ['story', 'combat', 'npc'] as const;
export type Task5RuntimeOptionScope =
(typeof TASK5_RUNTIME_OPTION_SCOPES)[number];
export type RuntimeStoryChoicePayload = JsonObject & {
optionText?: string;
note?: string;
};
export type RuntimeStoryOptionInteraction =
| {
kind: 'npc';
npcId: string;
action:
| 'chat'
| 'help'
| 'fight'
| 'leave'
| 'recruit'
| 'spar'
| 'trade'
| 'gift'
| 'quest_accept'
| 'quest_turn_in';
questId?: string;
}
| {
kind: 'treasure';
action: 'inspect' | 'leave' | 'secure';
};
export type RuntimeStoryChoiceAction = RuntimeAction<
'story_choice',
RuntimeStoryChoicePayload
> & {
functionId: string;
targetId?: string;
};
export type RuntimeStoryOptionView = {
functionId: string;
actionText: string;
detailText?: string;
scope: Task5RuntimeOptionScope;
interaction?: RuntimeStoryOptionInteraction;
payload?: RuntimeStoryChoicePayload;
disabled?: boolean;
reason?: string;
};
export type RuntimeStoryPlayerViewModel = {
hp: number;
maxHp: number;
mana: number;
maxMana: number;
};
export type RuntimeStoryCompanionViewModel = {
npcId: string;
characterId?: string;
joinedAtAffinity: number;
};
export type RuntimeStoryEncounterViewModel = {
id: string;
kind: 'npc' | 'treasure';
npcName: string;
hostile: boolean;
affinity?: number;
recruited?: boolean;
interactionActive: boolean;
battleMode?: 'fight' | 'spar' | null;
};
export type RuntimeStoryStatusViewModel = {
inBattle: boolean;
npcInteractionActive: boolean;
currentNpcBattleMode: 'fight' | 'spar' | null;
currentNpcBattleOutcome: 'fight_victory' | 'spar_complete' | null;
};
export type RuntimeBattlePresentation = {
targetId?: string;
targetName?: string;
damageDealt?: number;
damageTaken?: number;
outcome?: 'ongoing' | 'victory' | 'spar_complete' | 'escaped';
};
export type RuntimeStoryViewModel = {
player: RuntimeStoryPlayerViewModel;
encounter: RuntimeStoryEncounterViewModel | null;
companions: RuntimeStoryCompanionViewModel[];
availableOptions: RuntimeStoryOptionView[];
status: RuntimeStoryStatusViewModel;
};
export type RuntimeStoryPresentation = {
actionText: string;
resultText: string;
storyText: string;
options: RuntimeStoryOptionView[];
toast?: string | null;
battle?: RuntimeBattlePresentation | null;
};
export type RuntimeStoryPatch =
| {
type: 'story_history_append';
actionText: string;
resultText: string;
}
| {
type: 'npc_affinity_changed';
npcId: string;
previousAffinity: number;
nextAffinity: number;
}
| {
type: 'battle_resolved';
functionId: string;
targetId?: string;
damageDealt?: number;
damageTaken?: number;
outcome: 'ongoing' | 'victory' | 'spar_complete' | 'escaped';
}
| {
type: 'status_changed';
inBattle: boolean;
npcInteractionActive: boolean;
currentNpcBattleMode: 'fight' | 'spar' | null;
currentNpcBattleOutcome: 'fight_victory' | 'spar_complete' | null;
}
| {
type: 'encounter_changed';
encounterId: string | null;
};
export type RuntimeStoryActionRequest =
RuntimeActionRequest<RuntimeStoryChoiceAction>;
export type RuntimeStoryActionResponse<
TSnapshotGameState = JsonObject,
TSnapshotCurrentStory = JsonObject,
> = RuntimeActionResponse<
RuntimeStoryViewModel,
RuntimeStoryPresentation,
RuntimeStoryPatch
> & {
snapshot: SavedGameSnapshot<
TSnapshotGameState,
string,
TSnapshotCurrentStory
>;
};

View File

@@ -1,8 +1,19 @@
export * from './assets/qwenSprite';
export * from './contracts/auth';
export * from './contracts/common';
export type * from './contracts/customWorldAgent';
export * from './contracts/rpgAgentActions';
export * from './contracts/rpgAgentAnchors';
export * from './contracts/rpgAgentDraft';
export * from './contracts/rpgAgentSession';
export * from './contracts/rpgCreationFixtures';
export * from './contracts/rpgCreationPreview';
export * from './contracts/rpgCreationWorkSummary';
export * from './contracts/rpgRuntimeChat';
export * from './contracts/rpgRuntimeQuestAssist';
export * from './contracts/rpgRuntimeStoryAction';
export * from './contracts/rpgRuntimeStoryState';
export * from './contracts/runtime';
export * from './contracts/story';
export * from './http';
export * from './llm/narrativeLanguage';
export * from './llm/parsers';