This commit is contained in:
199
packages/shared/src/contracts/auth.ts
Normal file
199
packages/shared/src/contracts/auth.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
export type AuthBindingStatus = 'active' | 'pending_bind_phone';
|
||||
export type AuthLoginMethod = 'password' | 'phone' | 'wechat';
|
||||
|
||||
export type AuthUser = {
|
||||
id: string;
|
||||
publicUserCode: string;
|
||||
username: string;
|
||||
displayName: string;
|
||||
phoneNumberMasked: string | null;
|
||||
loginMethod: AuthLoginMethod;
|
||||
bindingStatus: AuthBindingStatus;
|
||||
wechatBound: boolean;
|
||||
};
|
||||
|
||||
export type PublicUserSummary = {
|
||||
id: string;
|
||||
publicUserCode: string;
|
||||
displayName: string;
|
||||
};
|
||||
|
||||
export type PublicUserSearchResponse = {
|
||||
user: PublicUserSummary;
|
||||
};
|
||||
|
||||
export type AuthEntryRequest = {
|
||||
phone: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type AuthEntryResponse = {
|
||||
token: string;
|
||||
user: AuthUser;
|
||||
};
|
||||
|
||||
export type AuthPasswordChangeRequest = {
|
||||
currentPassword?: string;
|
||||
newPassword: string;
|
||||
};
|
||||
|
||||
export type AuthPasswordChangeResponse = {
|
||||
user: AuthUser;
|
||||
};
|
||||
|
||||
export type AuthPasswordResetRequest = {
|
||||
phone: string;
|
||||
code: string;
|
||||
newPassword: string;
|
||||
};
|
||||
|
||||
export type AuthPasswordResetResponse = {
|
||||
token: string;
|
||||
user: AuthUser;
|
||||
};
|
||||
|
||||
export type AuthPhoneSendCodeRequest = {
|
||||
phone: string;
|
||||
scene?: 'login' | 'bind_phone' | 'change_phone';
|
||||
captchaChallengeId?: string;
|
||||
captchaAnswer?: string;
|
||||
};
|
||||
|
||||
export type AuthPhoneSendCodeResponse = {
|
||||
ok: true;
|
||||
cooldownSeconds: number;
|
||||
expiresInSeconds: number;
|
||||
providerRequestId: string | null;
|
||||
};
|
||||
|
||||
export type AuthPhoneLoginRequest = {
|
||||
phone: string;
|
||||
code: string;
|
||||
};
|
||||
|
||||
export type AuthPhoneLoginResponse = {
|
||||
token: string;
|
||||
user: AuthUser;
|
||||
};
|
||||
|
||||
export type AuthMeResponse = {
|
||||
user: AuthUser | null;
|
||||
availableLoginMethods: AuthLoginMethod[];
|
||||
};
|
||||
|
||||
export type AuthLoginOptionsResponse = {
|
||||
availableLoginMethods: AuthLoginMethod[];
|
||||
};
|
||||
|
||||
export type AuthWechatStartResponse = {
|
||||
authorizationUrl: string;
|
||||
};
|
||||
|
||||
export type AuthWechatBindPhoneRequest = {
|
||||
phone: string;
|
||||
code: string;
|
||||
};
|
||||
|
||||
export type AuthWechatBindPhoneResponse = {
|
||||
token: string;
|
||||
user: AuthUser;
|
||||
};
|
||||
|
||||
export type AuthPhoneChangeRequest = {
|
||||
phone: string;
|
||||
code: string;
|
||||
};
|
||||
|
||||
export type AuthPhoneChangeResponse = {
|
||||
user: AuthUser;
|
||||
};
|
||||
|
||||
export type AuthRefreshResponse = {
|
||||
ok: true;
|
||||
token?: string;
|
||||
};
|
||||
|
||||
export type AuthSessionSummary = {
|
||||
sessionId: string;
|
||||
clientType: string;
|
||||
clientRuntime: string;
|
||||
clientPlatform: string;
|
||||
clientLabel: string;
|
||||
deviceDisplayName: string;
|
||||
miniProgramAppId: string | null;
|
||||
miniProgramEnv: string | null;
|
||||
userAgent: string | null;
|
||||
ipMasked: string | null;
|
||||
isCurrent: boolean;
|
||||
createdAt: string;
|
||||
lastSeenAt: string;
|
||||
expiresAt: string;
|
||||
};
|
||||
|
||||
export type AuthSessionsResponse = {
|
||||
sessions: AuthSessionSummary[];
|
||||
};
|
||||
|
||||
export type AuthLogoutAllResponse = {
|
||||
ok: true;
|
||||
};
|
||||
|
||||
export type AuthRevokeSessionResponse = {
|
||||
ok: true;
|
||||
};
|
||||
|
||||
export type AuthAuditLogEventType =
|
||||
| 'password_login'
|
||||
| 'phone_login'
|
||||
| 'wechat_login'
|
||||
| 'wechat_bind_phone'
|
||||
| 'change_phone'
|
||||
| 'captcha_required'
|
||||
| 'logout'
|
||||
| 'logout_all'
|
||||
| 'revoke_session'
|
||||
| 'risk_block_phone'
|
||||
| 'risk_block_ip'
|
||||
| 'risk_unblock_phone'
|
||||
| 'risk_unblock_ip';
|
||||
|
||||
export type AuthAuditLogEntry = {
|
||||
id: string;
|
||||
eventType: AuthAuditLogEventType;
|
||||
title: string;
|
||||
detail: string;
|
||||
ipMasked: string | null;
|
||||
userAgent: string | null;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export type AuthAuditLogsResponse = {
|
||||
logs: AuthAuditLogEntry[];
|
||||
};
|
||||
|
||||
export type AuthCaptchaChallenge = {
|
||||
challengeId: string;
|
||||
promptText: string;
|
||||
imageDataUrl: string;
|
||||
expiresInSeconds: number;
|
||||
};
|
||||
|
||||
export type AuthRiskBlockSummary = {
|
||||
scopeType: 'phone' | 'ip';
|
||||
title: string;
|
||||
detail: string;
|
||||
expiresAt: string;
|
||||
remainingSeconds: number;
|
||||
};
|
||||
|
||||
export type AuthRiskBlocksResponse = {
|
||||
blocks: AuthRiskBlockSummary[];
|
||||
};
|
||||
|
||||
export type AuthLiftRiskBlockResponse = {
|
||||
ok: true;
|
||||
};
|
||||
|
||||
export type LogoutResponse = {
|
||||
ok: true;
|
||||
};
|
||||
191
packages/shared/src/contracts/bigFish.ts
Normal file
191
packages/shared/src/contracts/bigFish.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* 大鱼吃小鱼玩法域前端共享契约。
|
||||
* 字段与 server-rs/shared-contracts/src/big_fish.rs 保持 camelCase 对齐。
|
||||
*/
|
||||
export type CreateBigFishSessionRequest = {
|
||||
seedText?: string;
|
||||
};
|
||||
|
||||
export type SendBigFishMessageRequest = {
|
||||
clientMessageId: string;
|
||||
text: string;
|
||||
quickFillRequested?: boolean;
|
||||
};
|
||||
|
||||
export type BigFishActionId =
|
||||
| 'big_fish_compile_draft'
|
||||
| 'big_fish_generate_level_main_image'
|
||||
| 'big_fish_generate_level_motion'
|
||||
| 'big_fish_generate_stage_background'
|
||||
| 'big_fish_publish_game';
|
||||
|
||||
export type ExecuteBigFishActionRequest = {
|
||||
action: BigFishActionId;
|
||||
level?: number;
|
||||
motionKey?: 'idle_float' | 'move_swim' | string;
|
||||
};
|
||||
|
||||
export type SubmitBigFishInputRequest = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type BigFishAnchorStatus =
|
||||
| 'confirmed'
|
||||
| 'inferred'
|
||||
| 'missing'
|
||||
| 'locked'
|
||||
| string;
|
||||
|
||||
export type BigFishAnchorItemResponse = {
|
||||
key: string;
|
||||
label: string;
|
||||
value: string;
|
||||
status: BigFishAnchorStatus;
|
||||
};
|
||||
|
||||
export type BigFishAnchorPackResponse = {
|
||||
gameplayPromise: BigFishAnchorItemResponse;
|
||||
ecologyVisualTheme: BigFishAnchorItemResponse;
|
||||
growthLadder: BigFishAnchorItemResponse;
|
||||
riskTempo: BigFishAnchorItemResponse;
|
||||
};
|
||||
|
||||
export type BigFishLevelBlueprintResponse = {
|
||||
level: number;
|
||||
name: string;
|
||||
oneLineFantasy: string;
|
||||
silhouetteDirection: string;
|
||||
sizeRatio: number;
|
||||
visualPromptSeed: string;
|
||||
motionPromptSeed: string;
|
||||
mergeSourceLevel?: number | null;
|
||||
preyWindow: number[];
|
||||
threatWindow: number[];
|
||||
isFinalLevel: boolean;
|
||||
};
|
||||
|
||||
export type BigFishBackgroundBlueprintResponse = {
|
||||
theme: string;
|
||||
colorMood: string;
|
||||
foregroundHints: string;
|
||||
midgroundComposition: string;
|
||||
backgroundDepth: string;
|
||||
safePlayAreaHint: string;
|
||||
spawnEdgeHint: string;
|
||||
backgroundPromptSeed: string;
|
||||
};
|
||||
|
||||
export type BigFishRuntimeParamsResponse = {
|
||||
levelCount: number;
|
||||
mergeCountPerUpgrade: number;
|
||||
spawnTargetCount: number;
|
||||
leaderMoveSpeed: number;
|
||||
followerCatchUpSpeed: number;
|
||||
offscreenCullSeconds: number;
|
||||
preySpawnDeltaLevels: number[];
|
||||
threatSpawnDeltaLevels: number[];
|
||||
winLevel: number;
|
||||
};
|
||||
|
||||
export type BigFishGameDraftResponse = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
coreFun: string;
|
||||
ecologyTheme: string;
|
||||
levels: BigFishLevelBlueprintResponse[];
|
||||
background: BigFishBackgroundBlueprintResponse;
|
||||
runtimeParams: BigFishRuntimeParamsResponse;
|
||||
};
|
||||
|
||||
export type BigFishAgentMessageResponse = {
|
||||
id: string;
|
||||
role: 'user' | 'assistant' | string;
|
||||
kind: 'chat' | 'system' | 'warning' | string;
|
||||
text: string;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export type BigFishAssetKind =
|
||||
| 'level_main_image'
|
||||
| 'level_motion'
|
||||
| 'stage_background'
|
||||
| string;
|
||||
|
||||
export type BigFishAssetStatus = 'empty' | 'ready' | 'generating' | string;
|
||||
|
||||
export type BigFishAssetSlotResponse = {
|
||||
slotId: string;
|
||||
assetKind: BigFishAssetKind;
|
||||
level?: number | null;
|
||||
motionKey?: string | null;
|
||||
status: BigFishAssetStatus;
|
||||
assetUrl?: string | null;
|
||||
promptSnapshot: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type BigFishAssetCoverageResponse = {
|
||||
levelMainImageReadyCount: number;
|
||||
levelMotionReadyCount: number;
|
||||
backgroundReady: boolean;
|
||||
requiredLevelCount: number;
|
||||
publishReady: boolean;
|
||||
blockers: string[];
|
||||
};
|
||||
|
||||
export type BigFishSessionSnapshotResponse = {
|
||||
sessionId: string;
|
||||
currentTurn: number;
|
||||
progressPercent: number;
|
||||
stage: string;
|
||||
anchorPack: BigFishAnchorPackResponse;
|
||||
draft?: BigFishGameDraftResponse | null;
|
||||
assetSlots: BigFishAssetSlotResponse[];
|
||||
assetCoverage: BigFishAssetCoverageResponse;
|
||||
messages: BigFishAgentMessageResponse[];
|
||||
lastAssistantReply?: string | null;
|
||||
publishReady: boolean;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type BigFishSessionResponse = {
|
||||
session: BigFishSessionSnapshotResponse;
|
||||
};
|
||||
|
||||
export type BigFishActionResponse = {
|
||||
session: BigFishSessionSnapshotResponse;
|
||||
};
|
||||
|
||||
export type BigFishVector2Response = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type BigFishRuntimeEntityResponse = {
|
||||
entityId: string;
|
||||
level: number;
|
||||
position: BigFishVector2Response;
|
||||
radius: number;
|
||||
offscreenSeconds: number;
|
||||
};
|
||||
|
||||
export type BigFishRuntimeSnapshotResponse = {
|
||||
runId: string;
|
||||
sessionId: string;
|
||||
status: 'running' | 'won' | 'failed' | string;
|
||||
tick: number;
|
||||
playerLevel: number;
|
||||
winLevel: number;
|
||||
leaderEntityId?: string | null;
|
||||
ownedEntities: BigFishRuntimeEntityResponse[];
|
||||
wildEntities: BigFishRuntimeEntityResponse[];
|
||||
cameraCenter: BigFishVector2Response;
|
||||
lastInput: BigFishVector2Response;
|
||||
eventLog: string[];
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type BigFishRunResponse = {
|
||||
run: BigFishRuntimeSnapshotResponse;
|
||||
};
|
||||
21
packages/shared/src/contracts/bigFishWorkSummary.ts
Normal file
21
packages/shared/src/contracts/bigFishWorkSummary.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export type BigFishWorkStatus = 'draft' | 'published';
|
||||
|
||||
export interface BigFishWorkSummary {
|
||||
workId: string;
|
||||
sourceSessionId: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
summary: string;
|
||||
coverImageSrc: string | null;
|
||||
status: BigFishWorkStatus;
|
||||
updatedAt: string;
|
||||
publishReady: boolean;
|
||||
levelCount: number;
|
||||
levelMainImageReadyCount: number;
|
||||
levelMotionReadyCount: number;
|
||||
backgroundReady: boolean;
|
||||
}
|
||||
|
||||
export interface BigFishWorksResponse {
|
||||
items: BigFishWorkSummary[];
|
||||
}
|
||||
3
packages/shared/src/contracts/common.ts
Normal file
3
packages/shared/src/contracts/common.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export type JsonObject = Record<string, unknown>;
|
||||
|
||||
export type JsonArray = unknown[];
|
||||
16
packages/shared/src/contracts/creationAgentDocumentInput.ts
Normal file
16
packages/shared/src/contracts/creationAgentDocumentInput.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export interface ParseCreationAgentDocumentInputRequest {
|
||||
fileName: string;
|
||||
contentType?: string | null;
|
||||
contentBase64: string;
|
||||
}
|
||||
|
||||
export interface CreationAgentDocumentInputPayload {
|
||||
fileName: string;
|
||||
contentType?: string | null;
|
||||
sizeBytes: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface ParseCreationAgentDocumentInputResponse {
|
||||
document: CreationAgentDocumentInputPayload;
|
||||
}
|
||||
12
packages/shared/src/contracts/customWorldAgent.ts
Normal file
12
packages/shared/src/contracts/customWorldAgent.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* 兼容出口:
|
||||
* 当前仓库仍有大量旧 customWorld 命名导入,这个文件继续作为过渡层保留。
|
||||
* 工作包 H 完成后,真实类型定义已经迁移到 rpg* 契约文件中;这里仅聚合旧命名分文件。
|
||||
*/
|
||||
|
||||
export type * from './customWorldAgentAnchors';
|
||||
export type * from './customWorldAgentDraft';
|
||||
export type * from './customWorldAgentActions';
|
||||
export type * from './customWorldAgentSession';
|
||||
export type * from './customWorldResultPreview';
|
||||
export type * from './customWorldWorkSummary';
|
||||
14
packages/shared/src/contracts/customWorldAgentActions.ts
Normal file
14
packages/shared/src/contracts/customWorldAgentActions.ts
Normal 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';
|
||||
9
packages/shared/src/contracts/customWorldAgentAnchors.ts
Normal file
9
packages/shared/src/contracts/customWorldAgentAnchors.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* 旧 custom world 八锚点兼容出口。
|
||||
* 这里只保留旧命名到 RPG 创作域新契约的映射,便于旧导入渐进迁移。
|
||||
*/
|
||||
|
||||
export type {
|
||||
RpgCreationAnchorText as AnchorTextValue,
|
||||
RpgCreationAnchorContent as EightAnchorContent,
|
||||
} from './rpgAgentAnchors';
|
||||
29
packages/shared/src/contracts/customWorldAgentDraft.ts
Normal file
29
packages/shared/src/contracts/customWorldAgentDraft.ts
Normal 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';
|
||||
20
packages/shared/src/contracts/customWorldAgentSession.ts
Normal file
20
packages/shared/src/contracts/customWorldAgentSession.ts
Normal 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';
|
||||
12
packages/shared/src/contracts/customWorldResultPreview.ts
Normal file
12
packages/shared/src/contracts/customWorldResultPreview.ts
Normal 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';
|
||||
11
packages/shared/src/contracts/customWorldWorkSummary.ts
Normal file
11
packages/shared/src/contracts/customWorldWorkSummary.ts
Normal 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';
|
||||
65
packages/shared/src/contracts/puzzleAgentActions.ts
Normal file
65
packages/shared/src/contracts/puzzleAgentActions.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { PuzzleAgentSessionSnapshot } from './puzzleAgentSession';
|
||||
|
||||
export type PuzzleAgentSuggestedActionType =
|
||||
| 'request_summary'
|
||||
| 'compile_puzzle_draft'
|
||||
| 'generate_puzzle_images'
|
||||
| 'publish_puzzle_work';
|
||||
|
||||
export interface PuzzleAgentSuggestedAction {
|
||||
id: string;
|
||||
actionType: PuzzleAgentSuggestedActionType;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export type PuzzleAgentActionType =
|
||||
| 'compile_puzzle_draft'
|
||||
| 'generate_puzzle_images'
|
||||
| 'select_puzzle_image'
|
||||
| 'publish_puzzle_work';
|
||||
|
||||
export type PuzzleAgentOperationType =
|
||||
| 'process_message'
|
||||
| PuzzleAgentActionType;
|
||||
|
||||
export type PuzzleAgentOperationStatus =
|
||||
| 'queued'
|
||||
| 'running'
|
||||
| 'completed'
|
||||
| 'failed';
|
||||
|
||||
export interface PuzzleAgentOperationRecord {
|
||||
operationId: string;
|
||||
type: PuzzleAgentOperationType;
|
||||
status: PuzzleAgentOperationStatus;
|
||||
phaseLabel: string;
|
||||
phaseDetail: string;
|
||||
progress: number;
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
export type PuzzleAgentActionRequest =
|
||||
| { action: 'compile_puzzle_draft' }
|
||||
| {
|
||||
action: 'generate_puzzle_images';
|
||||
promptText?: string | null;
|
||||
candidateCount?: number;
|
||||
}
|
||||
| {
|
||||
action: 'select_puzzle_image';
|
||||
candidateId: string;
|
||||
}
|
||||
| {
|
||||
action: 'publish_puzzle_work';
|
||||
levelName?: string;
|
||||
summary?: string;
|
||||
themeTags?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* 拼图操作接口直接返回最新会话,避免前端在选图等轻操作后再额外 GET 大体积快照。
|
||||
*/
|
||||
export interface PuzzleAgentActionResponse {
|
||||
operation: PuzzleAgentOperationRecord;
|
||||
session: PuzzleAgentSessionSnapshot;
|
||||
}
|
||||
58
packages/shared/src/contracts/puzzleAgentDraft.ts
Normal file
58
packages/shared/src/contracts/puzzleAgentDraft.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import type { JsonObject } from './common';
|
||||
|
||||
export type PuzzleAnchorStatus =
|
||||
| 'missing'
|
||||
| 'inferred'
|
||||
| 'confirmed'
|
||||
| 'locked';
|
||||
|
||||
export interface PuzzleAnchorItem {
|
||||
key: string;
|
||||
label: string;
|
||||
value: string;
|
||||
status: PuzzleAnchorStatus;
|
||||
}
|
||||
|
||||
export interface PuzzleAnchorPack {
|
||||
themePromise: PuzzleAnchorItem;
|
||||
visualSubject: PuzzleAnchorItem;
|
||||
visualMood: PuzzleAnchorItem;
|
||||
compositionHooks: PuzzleAnchorItem;
|
||||
tagsAndForbidden: PuzzleAnchorItem;
|
||||
}
|
||||
|
||||
export interface PuzzleCreatorIntent {
|
||||
sourceMode: 'agent_chat';
|
||||
rawMessagesSummary: string;
|
||||
themePromise: string;
|
||||
visualSubject: string;
|
||||
visualMood: string[];
|
||||
compositionHooks: string[];
|
||||
themeTags: string[];
|
||||
forbiddenDirectives: string[];
|
||||
}
|
||||
|
||||
export interface PuzzleGeneratedImageCandidate {
|
||||
candidateId: string;
|
||||
imageSrc: string;
|
||||
assetId: string;
|
||||
prompt: string;
|
||||
actualPrompt?: string | null;
|
||||
sourceType: 'generated' | 'uploaded';
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
export interface PuzzleResultDraft {
|
||||
levelName: string;
|
||||
summary: string;
|
||||
themeTags: string[];
|
||||
forbiddenDirectives: string[];
|
||||
creatorIntent: PuzzleCreatorIntent | null;
|
||||
anchorPack: PuzzleAnchorPack;
|
||||
candidates: PuzzleGeneratedImageCandidate[];
|
||||
selectedCandidateId: string | null;
|
||||
coverImageSrc: string | null;
|
||||
coverAssetId: string | null;
|
||||
generationStatus: 'idle' | 'generating' | 'ready';
|
||||
metadata?: JsonObject | null;
|
||||
}
|
||||
59
packages/shared/src/contracts/puzzleAgentSession.ts
Normal file
59
packages/shared/src/contracts/puzzleAgentSession.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { PuzzleAgentActionResponse, PuzzleAgentSuggestedAction } from './puzzleAgentActions';
|
||||
import type { PuzzleAnchorPack, PuzzleResultDraft } from './puzzleAgentDraft';
|
||||
import type { PuzzleResultPreviewEnvelope } from './puzzleResultPreview';
|
||||
|
||||
export type PuzzleAgentStage =
|
||||
| 'collecting_anchors'
|
||||
| 'draft_ready'
|
||||
| 'image_refining'
|
||||
| 'ready_to_publish'
|
||||
| 'published';
|
||||
|
||||
export type PuzzleAgentMessageRole = 'user' | 'assistant' | 'system';
|
||||
|
||||
export type PuzzleAgentMessageKind =
|
||||
| 'chat'
|
||||
| 'summary'
|
||||
| 'action_result'
|
||||
| 'warning';
|
||||
|
||||
export interface PuzzleAgentMessage {
|
||||
id: string;
|
||||
role: PuzzleAgentMessageRole;
|
||||
kind: PuzzleAgentMessageKind;
|
||||
text: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface PuzzleAgentSessionSnapshot {
|
||||
sessionId: string;
|
||||
currentTurn: number;
|
||||
progressPercent: number;
|
||||
stage: PuzzleAgentStage;
|
||||
anchorPack: PuzzleAnchorPack;
|
||||
draft: PuzzleResultDraft | null;
|
||||
messages: PuzzleAgentMessage[];
|
||||
lastAssistantReply: string | null;
|
||||
publishedProfileId: string | null;
|
||||
suggestedActions: PuzzleAgentSuggestedAction[];
|
||||
resultPreview: PuzzleResultPreviewEnvelope | null;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CreatePuzzleAgentSessionRequest {
|
||||
seedText?: string;
|
||||
}
|
||||
|
||||
export interface CreatePuzzleAgentSessionResponse {
|
||||
session: PuzzleAgentSessionSnapshot;
|
||||
}
|
||||
|
||||
export interface SendPuzzleAgentMessageRequest {
|
||||
clientMessageId: string;
|
||||
text: string;
|
||||
quickFillRequested?: boolean;
|
||||
}
|
||||
|
||||
export interface SendPuzzleAgentMessageResponse extends PuzzleAgentActionResponse {
|
||||
session: PuzzleAgentSessionSnapshot;
|
||||
}
|
||||
21
packages/shared/src/contracts/puzzleResultPreview.ts
Normal file
21
packages/shared/src/contracts/puzzleResultPreview.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { PuzzleResultDraft } from './puzzleAgentDraft';
|
||||
|
||||
export interface PuzzleResultPreviewBlocker {
|
||||
id: string;
|
||||
code: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface PuzzleResultPreviewFinding {
|
||||
id: string;
|
||||
severity: 'info' | 'warning' | 'blocker';
|
||||
code: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface PuzzleResultPreviewEnvelope {
|
||||
draft: PuzzleResultDraft;
|
||||
blockers: PuzzleResultPreviewBlocker[];
|
||||
qualityFindings: PuzzleResultPreviewFinding[];
|
||||
publishReady: boolean;
|
||||
}
|
||||
79
packages/shared/src/contracts/puzzleRuntimeSession.ts
Normal file
79
packages/shared/src/contracts/puzzleRuntimeSession.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
export type PuzzleGridSize = 3 | 4;
|
||||
|
||||
export interface PuzzleCellPosition {
|
||||
row: number;
|
||||
col: number;
|
||||
}
|
||||
|
||||
export interface PuzzlePieceState {
|
||||
pieceId: string;
|
||||
correctRow: number;
|
||||
correctCol: number;
|
||||
currentRow: number;
|
||||
currentCol: number;
|
||||
mergedGroupId: string | null;
|
||||
}
|
||||
|
||||
export interface PuzzleMergedGroupState {
|
||||
groupId: string;
|
||||
pieceIds: string[];
|
||||
occupiedCells: PuzzleCellPosition[];
|
||||
}
|
||||
|
||||
export interface PuzzleBoardSnapshot {
|
||||
rows: number;
|
||||
cols: number;
|
||||
pieces: PuzzlePieceState[];
|
||||
mergedGroups: PuzzleMergedGroupState[];
|
||||
selectedPieceId: string | null;
|
||||
allTilesResolved: boolean;
|
||||
}
|
||||
|
||||
export interface PuzzleRuntimeLevelSnapshot {
|
||||
runId: string;
|
||||
levelIndex: number;
|
||||
gridSize: PuzzleGridSize;
|
||||
profileId: string;
|
||||
levelName: string;
|
||||
authorDisplayName: string;
|
||||
themeTags: string[];
|
||||
coverImageSrc: string | null;
|
||||
board: PuzzleBoardSnapshot;
|
||||
status: 'playing' | 'cleared';
|
||||
}
|
||||
|
||||
export interface PuzzleRunSnapshot {
|
||||
runId: string;
|
||||
entryProfileId: string;
|
||||
clearedLevelCount: number;
|
||||
currentLevelIndex: number;
|
||||
currentGridSize: PuzzleGridSize;
|
||||
playedProfileIds: string[];
|
||||
previousLevelTags: string[];
|
||||
currentLevel: PuzzleRuntimeLevelSnapshot | null;
|
||||
recommendedNextProfileId: string | null;
|
||||
}
|
||||
|
||||
export interface StartPuzzleRunRequest {
|
||||
profileId: string;
|
||||
}
|
||||
|
||||
export interface AdvanceLocalPuzzleNextLevelRequest {
|
||||
run: PuzzleRunSnapshot;
|
||||
sourceSessionId?: string | null;
|
||||
}
|
||||
|
||||
export interface PuzzleRunResponse {
|
||||
run: PuzzleRunSnapshot;
|
||||
}
|
||||
|
||||
export interface SwapPuzzlePiecesRequest {
|
||||
firstPieceId: string;
|
||||
secondPieceId: string;
|
||||
}
|
||||
|
||||
export interface DragPuzzlePieceRequest {
|
||||
pieceId: string;
|
||||
targetRow: number;
|
||||
targetCol: number;
|
||||
}
|
||||
39
packages/shared/src/contracts/puzzleWorkSummary.ts
Normal file
39
packages/shared/src/contracts/puzzleWorkSummary.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { JsonObject } from './common';
|
||||
import type { PuzzleAnchorPack } from './puzzleAgentDraft';
|
||||
|
||||
export type PuzzleWorkPublicationStatus = 'draft' | 'published';
|
||||
|
||||
export interface PuzzleWorkSummary {
|
||||
workId: string;
|
||||
profileId: string;
|
||||
ownerUserId: string;
|
||||
sourceSessionId?: string | null;
|
||||
authorDisplayName: string;
|
||||
levelName: string;
|
||||
summary: string;
|
||||
themeTags: string[];
|
||||
coverImageSrc: string | null;
|
||||
coverAssetId?: string | null;
|
||||
publicationStatus: PuzzleWorkPublicationStatus;
|
||||
updatedAt: string;
|
||||
publishedAt: string | null;
|
||||
playCount: number;
|
||||
publishReady: boolean;
|
||||
}
|
||||
|
||||
export interface PuzzleWorkProfile extends PuzzleWorkSummary {
|
||||
anchorPack: PuzzleAnchorPack;
|
||||
metadata?: JsonObject | null;
|
||||
}
|
||||
|
||||
export interface PuzzleWorksResponse {
|
||||
items: PuzzleWorkSummary[];
|
||||
}
|
||||
|
||||
export interface PuzzleWorkDetailResponse {
|
||||
item: PuzzleWorkProfile;
|
||||
}
|
||||
|
||||
export interface PuzzleWorkMutationResponse {
|
||||
item: PuzzleWorkProfile;
|
||||
}
|
||||
132
packages/shared/src/contracts/rpgAgentActions.ts
Normal file
132
packages/shared/src/contracts/rpgAgentActions.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* 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'
|
||||
| 'delete_characters'
|
||||
| 'delete_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;
|
||||
/** 操作创建时间,草稿生成进度页用它计算总耗时。 */
|
||||
startedAt?: string | null;
|
||||
updatedAt?: 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;
|
||||
roleType?: 'playable' | 'story' | null;
|
||||
promptText?: string | null;
|
||||
anchorCardIds?: string[];
|
||||
}
|
||||
| {
|
||||
action: 'generate_landmarks';
|
||||
count: number;
|
||||
promptText?: string | null;
|
||||
anchorCardIds?: string[];
|
||||
}
|
||||
| { action: 'delete_characters'; roleIds: string[] }
|
||||
| { action: 'delete_landmarks'; sceneIds: 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[];
|
||||
sceneKind?: 'camp' | 'landmark' | null;
|
||||
}
|
||||
| {
|
||||
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;
|
||||
}
|
||||
17
packages/shared/src/contracts/rpgAgentAnchors.ts
Normal file
17
packages/shared/src/contracts/rpgAgentAnchors.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* RPG 创作八锚点契约。
|
||||
* 每个锚点只保留一段凝练文本;细分关注点由 Agent prompt 负责,不再进入存储结构。
|
||||
*/
|
||||
|
||||
export type RpgCreationAnchorText = string | null;
|
||||
|
||||
export interface RpgCreationAnchorContent {
|
||||
worldPromise: RpgCreationAnchorText;
|
||||
playerFantasy: RpgCreationAnchorText;
|
||||
themeBoundary: RpgCreationAnchorText;
|
||||
playerEntryPoint: RpgCreationAnchorText;
|
||||
coreConflict: RpgCreationAnchorText;
|
||||
keyRelationships: RpgCreationAnchorText;
|
||||
hiddenLines: RpgCreationAnchorText;
|
||||
iconicElements: RpgCreationAnchorText;
|
||||
}
|
||||
279
packages/shared/src/contracts/rpgAgentDraft.ts
Normal file
279
packages/shared/src/contracts/rpgAgentDraft.ts
Normal file
@@ -0,0 +1,279 @@
|
||||
/**
|
||||
* 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;
|
||||
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;
|
||||
imageSrc?: string | null;
|
||||
generatedSceneAssetId?: string | null;
|
||||
generatedScenePrompt?: string | null;
|
||||
generatedSceneModel?: string | null;
|
||||
summary: string;
|
||||
}
|
||||
|
||||
export interface RpgAgentWorldAttributeSlot {
|
||||
slotId: 'axis_a' | 'axis_b' | 'axis_c' | 'axis_d' | 'axis_e' | 'axis_f';
|
||||
name: string;
|
||||
definition: string;
|
||||
positiveSignals: string[];
|
||||
negativeSignals: string[];
|
||||
combatUseText: string;
|
||||
socialUseText: string;
|
||||
explorationUseText: string;
|
||||
}
|
||||
|
||||
export interface RpgAgentWorldAttributeSchema {
|
||||
id: string;
|
||||
worldId: string;
|
||||
schemaVersion: number;
|
||||
generatedFrom: {
|
||||
worldType: 'CUSTOM' | 'WUXIA' | 'XIANXIA';
|
||||
worldName: string;
|
||||
settingSummary: string;
|
||||
tone: string;
|
||||
conflictCore: string;
|
||||
};
|
||||
schemaName?: string;
|
||||
slots: RpgAgentWorldAttributeSlot[];
|
||||
}
|
||||
|
||||
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;
|
||||
oppositeNpcId: string;
|
||||
eventDescription: string;
|
||||
linkedThreadIds: string[];
|
||||
actGoal: string;
|
||||
transitionHook: string;
|
||||
advanceRule: RpgAgentSceneActAdvanceRule;
|
||||
}
|
||||
|
||||
export interface RpgAgentFoundationDraftSceneChapter {
|
||||
id: string;
|
||||
sceneId: string;
|
||||
sceneName: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
sceneTaskDescription: 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;
|
||||
attributeSchema: RpgAgentWorldAttributeSchema;
|
||||
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;
|
||||
}
|
||||
134
packages/shared/src/contracts/rpgAgentSession.ts
Normal file
134
packages/shared/src/contracts/rpgAgentSession.ts
Normal 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;
|
||||
}
|
||||
146
packages/shared/src/contracts/rpgContracts.test.ts
Normal file
146
packages/shared/src/contracts/rpgContracts.test.ts
Normal 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).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,
|
||||
});
|
||||
});
|
||||
});
|
||||
766
packages/shared/src/contracts/rpgCreationFixtures.ts
Normal file
766
packages/shared/src/contracts/rpgCreationFixtures.ts
Normal file
@@ -0,0 +1,766 @@
|
||||
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:
|
||||
'被海雾吞没的旧航路群岛,灯塔与禁航令共同决定谁能活着穿过去,体验压抑、悬疑、潮湿。',
|
||||
playerFantasy:
|
||||
'玩家回到群岛调查沉船真相,核心追求是找出失控航路背后的真相,风险是失去最后一个还能对上旧案的人。',
|
||||
themeBoundary:
|
||||
'压抑、潮湿、悬疑;旧灯塔、潮雾、断裂航路;不要出现现代枪械。',
|
||||
playerEntryPoint:
|
||||
'玩家是被迫返乡的失职守灯人,首夜就有陌生船只闯入禁航区,动机是查清沉船夜里被谁改动了灯册。',
|
||||
coreConflict:
|
||||
'守灯会与航运公会争夺旧航路控制权,沉船夜的航灯与灯册被人动过手脚,玩家开局会撞上新的封航命令。',
|
||||
keyRelationships:
|
||||
'玩家与沈砺是旧友兼潜在背叛者,沈砺暗地里在替沉船商盟引路。',
|
||||
hiddenLines:
|
||||
'沉船夜的真实失误并不是单纯天灾;所有人都会先把问题推给潮雾本身;第一章露出痕迹,第二章才让玩家摸到灯册线。',
|
||||
iconicElements:
|
||||
'会移动的海雾、回潮旧灯塔、封灯令、旧潮图;禁航信号一旦点亮,任何船都必须退航。',
|
||||
} 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: '高处潮痕说明海面异常抬升过。',
|
||||
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: '克制、紧绷,但还能暂时收拢局势',
|
||||
imageSrc: '/custom/camp/huichao.png',
|
||||
generatedSceneAssetId: 'scene-asset-camp-1',
|
||||
summary: '玩家能在这里整理情报、回看旧灯册和沉船名单。',
|
||||
},
|
||||
themePack: {
|
||||
id: 'theme-pack:tide',
|
||||
displayName: '潮雾悬疑',
|
||||
},
|
||||
storyGraph: {
|
||||
visibleThreads: [
|
||||
{
|
||||
id: 'thread-visible-1',
|
||||
title: '封航争夺',
|
||||
},
|
||||
],
|
||||
},
|
||||
attributeSchema: {
|
||||
id: 'schema:rpg-agent:tide-fixture',
|
||||
worldId: 'custom:潮雾列岛',
|
||||
schemaVersion: 1,
|
||||
schemaName: '潮雾六脉',
|
||||
generatedFrom: {
|
||||
worldType: 'CUSTOM',
|
||||
worldName: '潮雾列岛',
|
||||
settingSummary: '旧灯塔与失控航路',
|
||||
tone: '压抑、潮湿、悬疑',
|
||||
conflictCore: '沉船夜的航灯与灯册被人动过手脚。',
|
||||
},
|
||||
slots: [
|
||||
{
|
||||
slotId: 'axis_a',
|
||||
name: '潮骨',
|
||||
definition: '承受潮压、封航令与正面冲击的底子。',
|
||||
positiveSignals: ['承压', '稳阵'],
|
||||
negativeSignals: ['散乱', '畏压'],
|
||||
combatUseText: '顶住正面压迫并守住行动空间。',
|
||||
socialUseText: '在封锁与质问中保持可信姿态。',
|
||||
explorationUseText: '穿过潮湿险境时维持身体与装备状态。',
|
||||
},
|
||||
{
|
||||
slotId: 'axis_b',
|
||||
name: '浪步',
|
||||
definition: '顺潮借势、换线穿行与抢占位置的能力。',
|
||||
positiveSignals: ['借势', '轻快'],
|
||||
negativeSignals: ['迟滞', '失位'],
|
||||
combatUseText: '借地形切线、拉开距离或抢先手。',
|
||||
socialUseText: '顺着对方语气调整节奏。',
|
||||
explorationUseText: '穿越港口、水路、雾区与复杂地形。',
|
||||
},
|
||||
{
|
||||
slotId: 'axis_c',
|
||||
name: '灯识',
|
||||
definition: '看懂灯号、潮痕、档案错页与人心遮掩的能力。',
|
||||
positiveSignals: ['辨伪', '识局'],
|
||||
negativeSignals: ['误读', '迟钝'],
|
||||
combatUseText: '识破破绽并判断局势变化。',
|
||||
socialUseText: '听出隐瞒、试探与交换空间。',
|
||||
explorationUseText: '辨认潮痕、灯册和沉船遗留线索。',
|
||||
},
|
||||
{
|
||||
slotId: 'axis_d',
|
||||
name: '雾魄',
|
||||
definition: '在海雾、旧案与封锁压力中推进真相的胆气。',
|
||||
positiveSignals: ['果断', '压前'],
|
||||
negativeSignals: ['犹疑', '退缩'],
|
||||
combatUseText: '顶着高压窗口推进突破口。',
|
||||
socialUseText: '在谈判或对峙中定调。',
|
||||
explorationUseText: '面对陌生雾区与异状仍敢继续前探。',
|
||||
},
|
||||
{
|
||||
slotId: 'axis_e',
|
||||
name: '旧约',
|
||||
definition: '与旧友、信物、灯令和地方关系建立牵引的能力。',
|
||||
positiveSignals: ['守诺', '通人情'],
|
||||
negativeSignals: ['疏离', '失信'],
|
||||
combatUseText: '借同伴协同与旧约牵制形成连锁。',
|
||||
socialUseText: '安抚、结盟、交换与维系信任。',
|
||||
explorationUseText: '从人情、传闻和旧物中打开线索。',
|
||||
},
|
||||
{
|
||||
slotId: 'axis_f',
|
||||
name: '回澜',
|
||||
definition: '在长线消耗中回稳节奏并维持判断的能力。',
|
||||
positiveSignals: ['回稳', '续航'],
|
||||
negativeSignals: ['紊乱', '断流'],
|
||||
combatUseText: '久战不乱,把节奏重新拉回手里。',
|
||||
socialUseText: '情绪稳定,不轻易被带偏。',
|
||||
explorationUseText: '在漫长远行与恶劣天气里保有余力。',
|
||||
},
|
||||
],
|
||||
},
|
||||
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: '围绕灯塔推进的首个场景章节。',
|
||||
sceneTaskDescription: '首次进入回潮旧灯塔时,追查禁航灯册为何被人改写。',
|
||||
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',
|
||||
oppositeNpcId: 'story-1',
|
||||
eventDescription: '顾潮音在旧灯塔门前拦住玩家,交出第一段灯册疑点。',
|
||||
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: '机动周旋',
|
||||
},
|
||||
],
|
||||
})),
|
||||
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,
|
||||
imageSrc: draft.camp?.imageSrc,
|
||||
},
|
||||
landmarks: draft.landmarks.map((landmark) => ({
|
||||
id: landmark.id,
|
||||
name: landmark.name,
|
||||
description: landmark.description,
|
||||
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,
|
||||
sceneTaskDescription: chapter.sceneTaskDescription,
|
||||
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,
|
||||
oppositeNpcId: act.oppositeNpcId,
|
||||
eventDescription: act.eventDescription,
|
||||
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: draftProfile as unknown as Record<string, unknown>,
|
||||
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,
|
||||
publicWorkCode: 'cw-fixture-001',
|
||||
authorPublicUserCode: 'sy-fixture-user',
|
||||
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);
|
||||
}
|
||||
40
packages/shared/src/contracts/rpgCreationPreview.ts
Normal file
40
packages/shared/src/contracts/rpgCreationPreview.ts
Normal 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;
|
||||
}
|
||||
38
packages/shared/src/contracts/rpgCreationWorkSummary.ts
Normal file
38
packages/shared/src/contracts/rpgCreationWorkSummary.ts
Normal 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[];
|
||||
}
|
||||
206
packages/shared/src/contracts/rpgRuntimeChat.ts
Normal file
206
packages/shared/src/contracts/rpgRuntimeChat.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* RPG 运行时聊天相关共享契约。
|
||||
* 将角色聊天、NPC 对话与轻量 story 请求载荷从旧 story.ts 中独立出来。
|
||||
*/
|
||||
import type { JsonObject } from './common';
|
||||
|
||||
export type NpcChatTurnLimitReason = 'negative_affinity';
|
||||
|
||||
export type NpcChatTurnClosingMode = 'free' | 'foreshadow_close';
|
||||
|
||||
export type NpcChatTurnTerminationMode = 'none' | 'hostile_model';
|
||||
|
||||
export type NpcChatTurnTerminationReason = 'hostile_breakoff' | 'player_exit';
|
||||
|
||||
export type NpcChatFunctionOption = {
|
||||
functionId: string;
|
||||
actionText: string;
|
||||
detailText?: string | null;
|
||||
action?: string | null;
|
||||
};
|
||||
|
||||
export type NpcChatTurnDirective = {
|
||||
sceneActId?: string | null;
|
||||
turnLimit?: number | null;
|
||||
remainingTurns?: number | null;
|
||||
limitReason?: NpcChatTurnLimitReason | null;
|
||||
closingMode?: NpcChatTurnClosingMode | null;
|
||||
forceExitAfterTurn?: boolean;
|
||||
terminationMode?: NpcChatTurnTerminationMode | null;
|
||||
terminationReason?: NpcChatTurnTerminationReason | null;
|
||||
isHostileChat?: boolean;
|
||||
functionOptions?: NpcChatFunctionOption[];
|
||||
};
|
||||
|
||||
export type NpcChatTurnCompletionDirective = {
|
||||
turnLimit?: number | null;
|
||||
remainingTurns?: number | null;
|
||||
forceExit?: boolean;
|
||||
closingMode?: NpcChatTurnClosingMode;
|
||||
terminationReason?: NpcChatTurnTerminationReason | null;
|
||||
};
|
||||
|
||||
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 NpcChatFunctionSuggestion = {
|
||||
functionId: string;
|
||||
actionText: string;
|
||||
};
|
||||
|
||||
export type NpcChatTurnResult<TQuest = unknown> = {
|
||||
npcReply: string;
|
||||
affinityDelta: number;
|
||||
affinityText: string;
|
||||
suggestions: string[];
|
||||
functionSuggestions?: NpcChatFunctionSuggestion[];
|
||||
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;
|
||||
};
|
||||
51
packages/shared/src/contracts/rpgRuntimeContracts.test.ts
Normal file
51
packages/shared/src/contracts/rpgRuntimeContracts.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
83
packages/shared/src/contracts/rpgRuntimeQuestAssist.ts
Normal file
83
packages/shared/src/contracts/rpgRuntimeQuestAssist.ts
Normal 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;
|
||||
};
|
||||
129
packages/shared/src/contracts/rpgRuntimeStoryAction.ts
Normal file
129
packages/shared/src/contracts/rpgRuntimeStoryAction.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* 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',
|
||||
] 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;
|
||||
};
|
||||
|
||||
export type RuntimeStoryChoiceAction = RuntimeAction<
|
||||
'story_choice',
|
||||
RuntimeStoryChoicePayload
|
||||
> & {
|
||||
functionId: string;
|
||||
targetId?: string;
|
||||
};
|
||||
146
packages/shared/src/contracts/rpgRuntimeStoryState.ts
Normal file
146
packages/shared/src/contracts/rpgRuntimeStoryState.ts
Normal 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
|
||||
>;
|
||||
};
|
||||
368
packages/shared/src/contracts/runtime.ts
Normal file
368
packages/shared/src/contracts/runtime.ts
Normal file
@@ -0,0 +1,368 @@
|
||||
import type { JsonObject } from './common';
|
||||
|
||||
export const SAVE_SNAPSHOT_VERSION = 2;
|
||||
export const DEFAULT_MUSIC_VOLUME = 0.42;
|
||||
export const DEFAULT_PLATFORM_THEME = 'light';
|
||||
export const PLATFORM_THEMES = ['light', 'dark'] as const;
|
||||
export type PlatformTheme = (typeof PLATFORM_THEMES)[number];
|
||||
|
||||
export type SavedGameSnapshot<
|
||||
TGameState = unknown,
|
||||
TBottomTab extends string = string,
|
||||
TCurrentStory = unknown,
|
||||
> = {
|
||||
version: number;
|
||||
savedAt: string;
|
||||
gameState: TGameState;
|
||||
bottomTab: TBottomTab;
|
||||
currentStory: TCurrentStory | null;
|
||||
};
|
||||
|
||||
export type SavedGameSnapshotInput<
|
||||
TGameState = unknown,
|
||||
TBottomTab extends string = string,
|
||||
TCurrentStory = unknown,
|
||||
> = Omit<
|
||||
SavedGameSnapshot<TGameState, TBottomTab, TCurrentStory>,
|
||||
'savedAt' | 'version'
|
||||
> & {
|
||||
savedAt?: string;
|
||||
};
|
||||
|
||||
export type RuntimeSettings = {
|
||||
musicVolume: number;
|
||||
platformTheme: PlatformTheme;
|
||||
};
|
||||
|
||||
export type BasicOkResult = {
|
||||
ok: true;
|
||||
};
|
||||
|
||||
export type ProfileDashboardCardKey = 'wallet' | 'playTime' | 'playedWorks';
|
||||
|
||||
export type ProfileDashboardSummary = {
|
||||
walletBalance: number;
|
||||
totalPlayTimeMs: number;
|
||||
playedWorldCount: number;
|
||||
updatedAt: string | null;
|
||||
};
|
||||
|
||||
export type ProfileWalletLedgerEntry = {
|
||||
id: string;
|
||||
amountDelta: number;
|
||||
balanceAfter: number;
|
||||
sourceType:
|
||||
| 'snapshot_sync'
|
||||
| 'invite_inviter_reward'
|
||||
| 'invite_invitee_reward'
|
||||
| 'points_recharge';
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export type ProfileWalletLedgerResponse = {
|
||||
entries: ProfileWalletLedgerEntry[];
|
||||
};
|
||||
|
||||
export type ProfileRechargeProductKind = 'points' | 'membership';
|
||||
export type ProfileMembershipStatus = 'normal' | 'active';
|
||||
export type ProfileMembershipTier = 'normal' | 'month' | 'season' | 'year';
|
||||
export type ProfileRechargeOrderStatus = 'paid';
|
||||
|
||||
export type ProfileRechargeProduct = {
|
||||
productId: string;
|
||||
title: string;
|
||||
priceCents: number;
|
||||
kind: ProfileRechargeProductKind;
|
||||
pointsAmount: number;
|
||||
bonusPoints: number;
|
||||
durationDays: number;
|
||||
badgeLabel: string;
|
||||
description: string;
|
||||
tier: ProfileMembershipTier;
|
||||
};
|
||||
|
||||
export type ProfileMembershipBenefit = {
|
||||
benefitName: string;
|
||||
normalValue: string;
|
||||
monthValue: string;
|
||||
seasonValue: string;
|
||||
yearValue: string;
|
||||
};
|
||||
|
||||
export type ProfileMembership = {
|
||||
status: ProfileMembershipStatus;
|
||||
tier: ProfileMembershipTier;
|
||||
startedAt: string | null;
|
||||
expiresAt: string | null;
|
||||
updatedAt: string | null;
|
||||
};
|
||||
|
||||
export type ProfileRechargeOrder = {
|
||||
orderId: string;
|
||||
productId: string;
|
||||
productTitle: string;
|
||||
kind: ProfileRechargeProductKind;
|
||||
amountCents: number;
|
||||
status: ProfileRechargeOrderStatus;
|
||||
paymentChannel: string;
|
||||
paidAt: string;
|
||||
createdAt: string;
|
||||
pointsDelta: number;
|
||||
membershipExpiresAt: string | null;
|
||||
};
|
||||
|
||||
export type ProfileRechargeCenterResponse = {
|
||||
walletBalance: number;
|
||||
membership: ProfileMembership;
|
||||
pointProducts: ProfileRechargeProduct[];
|
||||
membershipProducts: ProfileRechargeProduct[];
|
||||
benefits: ProfileMembershipBenefit[];
|
||||
latestOrder: ProfileRechargeOrder | null;
|
||||
hasPointsRecharged: boolean;
|
||||
};
|
||||
|
||||
export type CreateProfileRechargeOrderRequest = {
|
||||
productId: string;
|
||||
paymentChannel?: string;
|
||||
};
|
||||
|
||||
export type CreateProfileRechargeOrderResponse = {
|
||||
order: ProfileRechargeOrder;
|
||||
center: ProfileRechargeCenterResponse;
|
||||
};
|
||||
|
||||
export type ProfileReferralInviteCenterResponse = {
|
||||
inviteCode: string;
|
||||
inviteLinkPath: string;
|
||||
invitedCount: number;
|
||||
rewardedInviteCount: number;
|
||||
todayInviterRewardCount: number;
|
||||
todayInviterRewardRemaining: number;
|
||||
rewardPoints: number;
|
||||
hasRedeemedCode: boolean;
|
||||
boundInviterUserId: string | null;
|
||||
boundAt: string | null;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type RedeemProfileReferralInviteCodeRequest = {
|
||||
inviteCode: string;
|
||||
};
|
||||
|
||||
export type RedeemProfileReferralInviteCodeResponse = {
|
||||
center: ProfileReferralInviteCenterResponse;
|
||||
inviteeRewardGranted: boolean;
|
||||
inviterRewardGranted: boolean;
|
||||
inviteeBalanceAfter: number;
|
||||
inviterBalanceAfter: number;
|
||||
};
|
||||
|
||||
export type ProfilePlayedWorkSummary = {
|
||||
worldKey: string;
|
||||
ownerUserId: string | null;
|
||||
profileId: string | null;
|
||||
worldType: string | null;
|
||||
worldTitle: string;
|
||||
worldSubtitle: string;
|
||||
firstPlayedAt: string;
|
||||
lastPlayedAt: string;
|
||||
lastObservedPlayTimeMs: number;
|
||||
};
|
||||
|
||||
export type ProfilePlayStatsResponse = {
|
||||
totalPlayTimeMs: number;
|
||||
playedWorks: ProfilePlayedWorkSummary[];
|
||||
updatedAt: string | null;
|
||||
};
|
||||
|
||||
export type ProfileSaveArchiveSummary = {
|
||||
worldKey: string;
|
||||
ownerUserId: string | null;
|
||||
profileId: string | null;
|
||||
worldType: string | null;
|
||||
worldName: string;
|
||||
subtitle: string;
|
||||
summaryText: string;
|
||||
coverImageSrc: string | null;
|
||||
lastPlayedAt: string;
|
||||
};
|
||||
|
||||
export type ProfileSaveArchiveListResponse = {
|
||||
entries: ProfileSaveArchiveSummary[];
|
||||
};
|
||||
|
||||
export type ProfileSaveArchiveResumeResponse<
|
||||
TGameState = unknown,
|
||||
TBottomTab extends string = string,
|
||||
TCurrentStory = unknown,
|
||||
> = {
|
||||
entry: ProfileSaveArchiveSummary;
|
||||
snapshot: SavedGameSnapshot<TGameState, TBottomTab, TCurrentStory>;
|
||||
};
|
||||
|
||||
export type CustomWorldPublicationStatus = 'draft' | 'published';
|
||||
export type CustomWorldThemeMode =
|
||||
| 'martial'
|
||||
| 'arcane'
|
||||
| 'machina'
|
||||
| 'tide'
|
||||
| 'rift'
|
||||
| 'mythic';
|
||||
|
||||
export type CustomWorldProfileRecord = JsonObject & {
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export type CustomWorldLibraryEntry<TProfile = CustomWorldProfileRecord> = {
|
||||
ownerUserId: string;
|
||||
profileId: string;
|
||||
publicWorkCode: string | null;
|
||||
authorPublicUserCode: string | null;
|
||||
profile: TProfile;
|
||||
visibility: CustomWorldPublicationStatus;
|
||||
publishedAt: string | null;
|
||||
updatedAt: string;
|
||||
authorDisplayName: string;
|
||||
worldName: string;
|
||||
subtitle: string;
|
||||
summaryText: string;
|
||||
coverImageSrc: string | null;
|
||||
themeMode: CustomWorldThemeMode;
|
||||
playableNpcCount: number;
|
||||
landmarkCount: number;
|
||||
};
|
||||
|
||||
export type CustomWorldGalleryCard = Omit<
|
||||
CustomWorldLibraryEntry<never>,
|
||||
'profile'
|
||||
>;
|
||||
|
||||
export type CustomWorldLibraryResponse<TProfile = CustomWorldProfileRecord> = {
|
||||
entries: CustomWorldLibraryEntry<TProfile>[];
|
||||
};
|
||||
|
||||
export type CustomWorldLibraryMutationResponse<
|
||||
TProfile = CustomWorldProfileRecord,
|
||||
> = {
|
||||
entry: CustomWorldLibraryEntry<TProfile>;
|
||||
entries: CustomWorldLibraryEntry<TProfile>[];
|
||||
};
|
||||
|
||||
export type CustomWorldGalleryResponse = {
|
||||
entries: CustomWorldGalleryCard[];
|
||||
};
|
||||
|
||||
export type CustomWorldGalleryDetailResponse<
|
||||
TProfile = CustomWorldProfileRecord,
|
||||
> = {
|
||||
entry: CustomWorldLibraryEntry<TProfile>;
|
||||
};
|
||||
|
||||
export type PlatformBrowseHistoryEntry = {
|
||||
ownerUserId: string;
|
||||
profileId: string;
|
||||
worldName: string;
|
||||
subtitle: string;
|
||||
summaryText: string;
|
||||
coverImageSrc: string | null;
|
||||
themeMode: CustomWorldThemeMode;
|
||||
authorDisplayName: string;
|
||||
visitedAt: string;
|
||||
};
|
||||
|
||||
export type PlatformBrowseHistoryWriteEntry = Omit<
|
||||
PlatformBrowseHistoryEntry,
|
||||
'visitedAt'
|
||||
> & {
|
||||
visitedAt?: string;
|
||||
};
|
||||
|
||||
export type PlatformBrowseHistoryResponse = {
|
||||
entries: PlatformBrowseHistoryEntry[];
|
||||
};
|
||||
|
||||
export type PlatformBrowseHistoryBatchSyncRequest = {
|
||||
entries: PlatformBrowseHistoryWriteEntry[];
|
||||
};
|
||||
|
||||
export const CUSTOM_WORLD_GENERATION_MODES = ['fast', 'full'] as const;
|
||||
export type CustomWorldGenerationMode =
|
||||
(typeof CUSTOM_WORLD_GENERATION_MODES)[number];
|
||||
|
||||
export const CUSTOM_WORLD_SESSION_STATUSES = [
|
||||
'clarifying',
|
||||
'ready_to_generate',
|
||||
'generating',
|
||||
'completed',
|
||||
'generation_error',
|
||||
] as const;
|
||||
export type CustomWorldSessionStatus =
|
||||
(typeof CUSTOM_WORLD_SESSION_STATUSES)[number];
|
||||
|
||||
export type CustomWorldQuestion = {
|
||||
id: string;
|
||||
label: string;
|
||||
question: string;
|
||||
answer?: string;
|
||||
};
|
||||
|
||||
export type CustomWorldGenerationStep = {
|
||||
id: string;
|
||||
label: string;
|
||||
detail: string;
|
||||
completed: number;
|
||||
total: number;
|
||||
status: 'pending' | 'active' | 'completed';
|
||||
};
|
||||
|
||||
export type CustomWorldGenerationProgress = {
|
||||
phaseId: string;
|
||||
phaseLabel: string;
|
||||
phaseDetail: string;
|
||||
batchLabel?: string;
|
||||
overallProgress: number;
|
||||
completedWeight: number;
|
||||
totalWeight: number;
|
||||
elapsedMs: number;
|
||||
estimatedRemainingMs: number | null;
|
||||
activeStepIndex: number;
|
||||
steps: CustomWorldGenerationStep[];
|
||||
};
|
||||
|
||||
export type GenerateCustomWorldProfileOptions = {
|
||||
onProgress?: (progress: CustomWorldGenerationProgress) => void;
|
||||
signal?: AbortSignal;
|
||||
};
|
||||
|
||||
export type GenerateCustomWorldProfileInput = {
|
||||
settingText: string;
|
||||
creatorIntent?: JsonObject | null;
|
||||
generationMode?: CustomWorldGenerationMode;
|
||||
};
|
||||
|
||||
export type CreateCustomWorldSessionRequest = {
|
||||
settingText: string;
|
||||
creatorIntent?: JsonObject | null;
|
||||
generationMode: CustomWorldGenerationMode;
|
||||
};
|
||||
|
||||
export type AnswerCustomWorldSessionQuestionRequest = {
|
||||
questionId: string;
|
||||
answer: string;
|
||||
};
|
||||
|
||||
export type CustomWorldSessionSummary = {
|
||||
sessionId: string;
|
||||
status: CustomWorldSessionStatus;
|
||||
questions: CustomWorldQuestion[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type CustomWorldSessionRecord = CustomWorldSessionSummary & {
|
||||
settingText: string;
|
||||
creatorIntent?: JsonObject | null;
|
||||
generationMode: CustomWorldGenerationMode;
|
||||
result?: JsonObject;
|
||||
lastError?: string;
|
||||
};
|
||||
Reference in New Issue
Block a user