This commit is contained in:
2026-04-25 22:19:04 +08:00
parent 2ebfd1cf55
commit 8404081d7b
149 changed files with 10508 additions and 2732 deletions

View File

@@ -7,16 +7,16 @@
useState,
} from 'react';
import type {
EightAnchorContent,
KeyRelationshipValue,
} from '../../packages/shared/src/contracts/customWorldAgent';
import { getCustomWorldSceneRelativePositionLabel } from '../data/customWorldSceneGraph';
import {
resolveCustomWorldCampSceneImage,
resolveCustomWorldLandmarkImageMap,
} from '../data/customWorldVisuals';
import { resolveCustomWorldCampScene } from '../services/customWorldCamp';
import {
buildCustomWorldFoundationEntries,
parseFoundationTagText,
} from '../services/customWorldFoundationEntries';
import { normalizeCustomWorldCreatorIntent } from '../services/customWorldCreatorIntent';
import {
AnimationState,
@@ -557,226 +557,6 @@ function resolvePlayableRolePreviewImage(
return '';
}
function toText(value: unknown) {
return typeof value === 'string' ? value.trim() : '';
}
function toTextArray(value: unknown) {
return Array.isArray(value)
? value.map((item) => toText(item)).filter(Boolean)
: [];
}
function toRecord(value: unknown) {
return value && typeof value === 'object' && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
function buildRelationshipSeedText(value: unknown) {
const record = toRecord(value);
if (!record) {
return '';
}
return compactTextList([
toText(record.name),
toText(record.role),
toText(record.relationToPlayer)
? `与玩家:${toText(record.relationToPlayer)}`
: '',
toText(record.hiddenHook) ? `代价/暗线:${toText(record.hiddenHook)}` : '',
]).join('');
}
function buildKeyRelationshipText(value: KeyRelationshipValue) {
return compactTextList([
value.pairs,
value.relationshipType,
value.secretOrCost ? `代价/秘密:${value.secretOrCost}` : '',
]).join('');
}
function buildAnchorContentFromProfileFallback(
profile: CustomWorldProfile,
): EightAnchorContent {
const creatorIntent = normalizeCustomWorldCreatorIntent(profile.creatorIntent);
const relationshipSeed = creatorIntent?.keyCharacters[0] ?? null;
return {
worldPromise: {
hook:
creatorIntent?.worldHook ||
profile.anchorPack?.worldSummary ||
profile.summary,
differentiator: profile.subtitle || profile.settingText,
desiredExperience:
compactTextList([
creatorIntent?.toneDirectives.join('、') || '',
profile.tone,
]).join('') || profile.tone,
},
playerFantasy: {
playerRole: creatorIntent?.playerPremise || profile.playerGoal,
corePursuit: profile.playerGoal,
fearOfLoss:
relationshipSeed?.hiddenHook ||
creatorIntent?.coreConflicts[0] ||
profile.coreConflicts[0] ||
'',
},
themeBoundary: {
toneKeywords: compactTextList([
creatorIntent?.themeKeywords.join('、') || '',
creatorIntent?.toneDirectives.join('、') || '',
]),
aestheticDirectives: compactTextList([profile.tone, profile.subtitle]),
forbiddenDirectives: creatorIntent?.forbiddenDirectives ?? [],
},
playerEntryPoint: {
openingIdentity: creatorIntent?.playerPremise || '',
openingProblem:
creatorIntent?.openingSituation || profile.coreConflicts[0] || '',
entryMotivation: profile.playerGoal,
},
coreConflict: {
surfaceConflicts:
creatorIntent?.coreConflicts.length
? creatorIntent.coreConflicts
: profile.coreConflicts,
hiddenCrisis:
relationshipSeed?.hiddenHook ||
profile.summary ||
profile.settingText,
firstTouchedConflict:
creatorIntent?.openingSituation ||
profile.coreConflicts[0] ||
profile.playerGoal,
},
keyRelationships: relationshipSeed
? [
{
pairs: compactTextList([
relationshipSeed.name,
relationshipSeed.role,
]).join(' · '),
relationshipType: relationshipSeed.relationToPlayer || '',
secretOrCost: relationshipSeed.hiddenHook || '',
},
]
: [],
hiddenLines: {
hiddenTruths: compactTextList([
relationshipSeed?.hiddenHook || '',
profile.summary,
]),
misdirectionHints: compactTextList([
profile.subtitle,
profile.majorFactions[0] || '',
]),
revealPacing:
creatorIntent?.openingSituation ||
profile.coreConflicts[0] ||
profile.playerGoal,
},
iconicElements: {
iconicMotifs:
creatorIntent?.iconicElements.length
? creatorIntent.iconicElements
: compactTextList([
profile.anchorPack?.motifDirectives.join('、') || '',
profile.landmarks[0]?.name || '',
]),
institutionsOrArtifacts: compactTextList([
profile.camp?.name || '',
profile.majorFactions[0] || '',
]),
hardRules: compactTextList([profile.playerGoal, profile.coreConflicts[0] || '']),
},
};
}
function getProfileAnchorContent(profile: CustomWorldProfile) {
const anchorContentRecord = profile.anchorContent;
if (!anchorContentRecord) {
return buildAnchorContentFromProfileFallback(profile);
}
const worldPromiseRecord = toRecord(anchorContentRecord.worldPromise);
const playerFantasyRecord = toRecord(anchorContentRecord.playerFantasy);
const themeBoundaryRecord = toRecord(anchorContentRecord.themeBoundary);
const playerEntryPointRecord = toRecord(anchorContentRecord.playerEntryPoint);
const coreConflictRecord = toRecord(anchorContentRecord.coreConflict);
const hiddenLinesRecord = toRecord(anchorContentRecord.hiddenLines);
const iconicElementsRecord = toRecord(anchorContentRecord.iconicElements);
return {
worldPromise: worldPromiseRecord
? {
hook: toText(worldPromiseRecord.hook),
differentiator: toText(worldPromiseRecord.differentiator),
desiredExperience: toText(worldPromiseRecord.desiredExperience),
}
: null,
playerFantasy: playerFantasyRecord
? {
playerRole: toText(playerFantasyRecord.playerRole),
corePursuit: toText(playerFantasyRecord.corePursuit),
fearOfLoss: toText(playerFantasyRecord.fearOfLoss),
}
: null,
themeBoundary: themeBoundaryRecord
? {
toneKeywords: toTextArray(themeBoundaryRecord.toneKeywords),
aestheticDirectives: toTextArray(
themeBoundaryRecord.aestheticDirectives,
),
forbiddenDirectives: toTextArray(themeBoundaryRecord.forbiddenDirectives),
}
: null,
playerEntryPoint: playerEntryPointRecord
? {
openingIdentity: toText(playerEntryPointRecord.openingIdentity),
openingProblem: toText(playerEntryPointRecord.openingProblem),
entryMotivation: toText(playerEntryPointRecord.entryMotivation),
}
: null,
coreConflict: coreConflictRecord
? {
surfaceConflicts: toTextArray(coreConflictRecord.surfaceConflicts),
hiddenCrisis: toText(coreConflictRecord.hiddenCrisis),
firstTouchedConflict: toText(coreConflictRecord.firstTouchedConflict),
}
: null,
keyRelationships: Array.isArray(anchorContentRecord.keyRelationships)
? anchorContentRecord.keyRelationships
.map((entry) => toRecord(entry))
.filter(Boolean)
.map((entry) => ({
pairs: toText(entry?.pairs),
relationshipType: toText(entry?.relationshipType),
secretOrCost: toText(entry?.secretOrCost),
}))
: [],
hiddenLines: hiddenLinesRecord
? {
hiddenTruths: toTextArray(hiddenLinesRecord.hiddenTruths),
misdirectionHints: toTextArray(hiddenLinesRecord.misdirectionHints),
revealPacing: toText(hiddenLinesRecord.revealPacing),
}
: null,
iconicElements: iconicElementsRecord
? {
iconicMotifs: toTextArray(iconicElementsRecord.iconicMotifs),
institutionsOrArtifacts: toTextArray(
iconicElementsRecord.institutionsOrArtifacts,
),
hardRules: toTextArray(iconicElementsRecord.hardRules),
}
: null,
} satisfies EightAnchorContent;
}
function buildOpeningSceneSearchText(
profile: CustomWorldProfile,
campScene: ReturnType<typeof resolveCustomWorldCampScene>,
@@ -791,91 +571,6 @@ function buildOpeningSceneSearchText(
].join(' ');
}
function buildStructuredFoundationEntries(profile: CustomWorldProfile) {
const creatorIntent = normalizeCustomWorldCreatorIntent(profile.creatorIntent);
const anchorContent = getProfileAnchorContent(profile);
const fallbackRelationshipText =
buildRelationshipSeedText(creatorIntent?.keyCharacters[0]) ||
profile.playableNpcs[0]?.relationshipHooks.join('') ||
profile.storyNpcs[0]?.relationshipHooks.join('') ||
'';
return [
{
id: 'world-promise',
label: '世界承诺',
value: compactTextList([
anchorContent.worldPromise?.hook || '',
anchorContent.worldPromise?.differentiator || '',
anchorContent.worldPromise?.desiredExperience || '',
]).join(''),
},
{
id: 'player-fantasy',
label: '玩家幻想',
value: compactTextList([
anchorContent.playerFantasy?.playerRole || '',
anchorContent.playerFantasy?.corePursuit || '',
anchorContent.playerFantasy?.fearOfLoss || '',
]).join(''),
},
{
id: 'theme-boundary',
label: '主题边界',
value: compactTextList([
anchorContent.themeBoundary?.toneKeywords.join('、') || '',
anchorContent.themeBoundary?.aestheticDirectives.join('、') || '',
anchorContent.themeBoundary?.forbiddenDirectives.length
? `避免:${anchorContent.themeBoundary.forbiddenDirectives.join('、')}`
: '',
]).join(''),
},
{
id: 'player-entry-point',
label: '玩家切入口',
value: compactTextList([
anchorContent.playerEntryPoint?.openingIdentity || '',
anchorContent.playerEntryPoint?.openingProblem || '',
anchorContent.playerEntryPoint?.entryMotivation || '',
]).join(''),
},
{
id: 'core-conflict',
label: '核心冲突',
value: compactTextList([
anchorContent.coreConflict?.surfaceConflicts.join('、') || '',
anchorContent.coreConflict?.hiddenCrisis || '',
anchorContent.coreConflict?.firstTouchedConflict || '',
]).join(''),
},
{
id: 'key-relationships',
label: '关键关系',
value:
anchorContent.keyRelationships.map(buildKeyRelationshipText).join('\n') ||
fallbackRelationshipText,
},
{
id: 'hidden-lines',
label: '暗线与揭示',
value: compactTextList([
anchorContent.hiddenLines?.hiddenTruths.join('、') || '',
anchorContent.hiddenLines?.misdirectionHints.join('、') || '',
anchorContent.hiddenLines?.revealPacing || '',
]).join(''),
},
{
id: 'iconic-elements',
label: '标志元素',
value: compactTextList([
anchorContent.iconicElements?.iconicMotifs.join('、') || '',
anchorContent.iconicElements?.institutionsOrArtifacts.join('、') || '',
anchorContent.iconicElements?.hardRules.join('、') || '',
]).join(''),
},
];
}
type CatalogRole =
| CustomWorldProfile['playableNpcs'][number]
| CustomWorldProfile['storyNpcs'][number];
@@ -1042,7 +737,7 @@ export function CustomWorldEntityCatalog({
[deferredSearch, landmarkById, profile.landmarks, storyNpcById],
);
const structuredFoundationEntries = useMemo(
() => buildStructuredFoundationEntries(profile),
() => buildCustomWorldFoundationEntries(profile),
[profile],
);
const normalizedCreatorIntent = useMemo(
@@ -1376,14 +1071,14 @@ export function CustomWorldEntityCatalog({
actions={
readOnly ? (
<SmallButton
onClick={() => onEditTarget({ kind: 'world' })}
onClick={() => onEditTarget({ kind: 'foundation' })}
tone="sky"
>
</SmallButton>
) : (
<SmallButton
onClick={() => onEditTarget({ kind: 'world' })}
onClick={() => onEditTarget({ kind: 'foundation' })}
tone="sky"
>
@@ -1401,9 +1096,22 @@ export function CustomWorldEntityCatalog({
<div className="text-[11px] font-bold tracking-[0.18em] text-zinc-500">
{entry.label}
</div>
<div className="mt-2 whitespace-pre-line text-sm leading-7 text-zinc-100">
{entry.value || '待补充'}
</div>
{entry.value ? (
<div className="mt-3 flex flex-wrap gap-2">
{parseFoundationTagText(entry.value).map((tag, index) => (
<span
key={`${entry.id}-${index}-${tag}`}
className="rounded-full border border-white/10 bg-white/[0.06] px-3 py-1 text-xs leading-5 text-zinc-100"
>
{tag}
</span>
))}
</div>
) : (
<div className="mt-2 text-sm leading-7 text-zinc-100">
</div>
)}
</div>
))}
</div>

View File

@@ -479,6 +479,81 @@ test('场景角色修改后右上角关闭才弹确认', async () => {
expect(screen.getAllByText('确认关闭').length).toBeGreaterThan(0);
});
test('世界页基本设定编辑按钮打开基本设定编辑目标', async () => {
const user = userEvent.setup();
const handleEditTarget = vi.fn();
render(
<CustomWorldEntityCatalog
profile={createProfile()}
previewCharacters={[]}
activeTab="world"
onActiveTabChange={() => {}}
onEditTarget={handleEditTarget}
onProfileChange={() => {}}
/>,
);
const editButtons = screen.getAllByRole('button', { name: '编辑' });
const foundationEditButton = editButtons[1];
expect(foundationEditButton).toBeDefined();
await user.click(foundationEditButton as HTMLElement);
expect(handleEditTarget).toHaveBeenCalledWith({ kind: 'foundation' });
});
test('基本设定用分号拆分成标签展示', () => {
const profile = {
...createProfile(),
anchorContent: {
worldPromise: {
hook: '机械微生物吞并进化',
differentiator: '角色被迫寄生改造',
desiredExperience: '在失控系统里求生',
},
playerFantasy: null,
themeBoundary: null,
playerEntryPoint: null,
coreConflict: null,
keyRelationships: [],
hiddenLines: null,
iconicElements: null,
},
} as CustomWorldProfile;
render(
<CustomWorldEntityCatalog
profile={profile}
previewCharacters={[]}
activeTab="world"
onActiveTabChange={() => {}}
onEditTarget={() => {}}
onProfileChange={() => {}}
/>,
);
const foundationSection = screen.getByText('世界承诺').closest('div');
expect(foundationSection).not.toBeNull();
expect(screen.getByText('机械微生物吞并进化')).toBeTruthy();
expect(screen.getByText('角色被迫寄生改造')).toBeTruthy();
expect(screen.getByText('在失控系统里求生')).toBeTruthy();
});
test('基本设定目标打开独立编辑面板', () => {
render(
<RpgCreationEntityEditorModal
profile={createProfile()}
target={{ kind: 'foundation' }}
onClose={vi.fn()}
onProfileChange={vi.fn()}
/>,
);
expect(screen.getByText('编辑基本设定')).toBeTruthy();
expect(screen.queryByText('编辑世界信息')).toBeNull();
});
test('可扮演角色列表使用缩略卡片并点击进入编辑', async () => {
const user = userEvent.setup();
const handleEditTarget = vi.fn();

View File

@@ -20,6 +20,7 @@ import {
type BigFishAgentWorkspaceProps = {
session: BigFishSessionSnapshotResponse | null;
streamingReplyText?: string;
isStreamingReply?: boolean;
isBusy?: boolean;
error?: string | null;
onBack: () => void;
@@ -72,6 +73,7 @@ function mapBigFishSession(
export function BigFishAgentWorkspace({
session,
streamingReplyText = '',
isStreamingReply = false,
isBusy = false,
error = null,
onBack,
@@ -86,7 +88,7 @@ export function BigFishAgentWorkspace({
composerPlaceholder="说说这局的生态、成长或爽点..."
primaryActionLabel="生成结果页"
streamingReplyText={streamingReplyText}
isStreamingReply={Boolean(streamingReplyText)}
isStreamingReply={isStreamingReply}
isBusy={isBusy}
error={error}
quickActions={createCreationAgentChatQuickActions()}

View File

@@ -1,10 +1,10 @@
/* @vitest-environment jsdom */
import { fireEvent, render, screen } from '@testing-library/react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { afterEach, expect, test, vi } from 'vitest';
import * as creationAgentServices from '../../services/creation-agent';
import { createCreationAgentChatQuickActions } from '../../services/creation-agent';
import {
type CreationAgentTheme,
CreationAgentWorkspace,
@@ -58,9 +58,9 @@ test('creation agent workspace keeps initial chat progress at zero percent', ()
const progressbar = screen.getByRole('progressbar');
expect(progressbar.getAttribute('aria-valuenow')).toBe('0');
expect((progressbar.firstElementChild as HTMLElement | null)?.style.width).toBe(
'0%',
);
expect(
(progressbar.firstElementChild as HTMLElement | null)?.style.width,
).toBe('0%');
});
test('creation agent workspace filters duplicate recommended replies', () => {
@@ -153,6 +153,41 @@ test('creation agent workspace renders streaming assistant text', () => {
expect(screen.getByText(//u)).toBeTruthy();
});
test('creation agent workspace renders waiting dots before first streamed token', () => {
ensureScrollApis();
render(
<CreationAgentWorkspace
session={{
sessionId: 'creation-agent-session-1',
title: '统一共创',
currentTurn: 1,
progressPercent: 20,
anchors: [],
messages: [
{
id: 'message-1',
role: 'user',
kind: 'chat',
text: '我想做一个潮湿压抑的海上世界。',
},
],
}}
theme={testTheme}
loadingText="正在准备"
composerPlaceholder="输入消息"
primaryActionLabel="生成结果页"
streamingReplyText=""
isStreamingReply
onBack={() => {}}
onSubmitText={() => {}}
onPrimaryAction={() => {}}
/>,
);
expect(screen.getByTestId('creation-agent-waiting-dots')).toBeTruthy();
});
test('creation agent workspace appends streaming assistant message after stable message list', () => {
ensureScrollApis();
@@ -200,7 +235,9 @@ test('creation agent workspace appends streaming assistant message after stable
const bubbles = screen
.getByTestId('creation-agent-message-list')
.querySelectorAll('.whitespace-pre-wrap');
const bubbleTexts = Array.from(bubbles).map((node) => node.textContent?.trim());
const bubbleTexts = Array.from(bubbles).map((node) =>
node.textContent?.trim(),
);
expect(bubbleTexts).toEqual([
'我想做一个潮湿压抑的海上世界。',
@@ -411,3 +448,104 @@ test('creation agent workspace stops auto-follow when user scrolls away from bot
expect(scrollToSpy).not.toHaveBeenCalled();
});
test('creation agent workspace appends parsed document text into composer', async () => {
ensureScrollApis();
vi.spyOn(
creationAgentServices,
'parseCreationAgentDocumentInput',
).mockResolvedValue({
document: {
fileName: '世界设定.md',
contentType: 'text/markdown',
sizeBytes: 24,
text: '第一章:潮湿的港口',
},
});
render(
<CreationAgentWorkspace
session={{
sessionId: 'creation-agent-session-1',
title: null,
currentTurn: 0,
progressPercent: 0,
anchors: [],
messages: [],
}}
theme={testTheme}
loadingText="正在准备"
composerPlaceholder="输入消息"
primaryActionLabel="生成结果页"
onBack={() => {}}
onSubmitText={() => {}}
onPrimaryAction={() => {}}
/>,
);
fireEvent.change(screen.getByPlaceholderText('输入消息'), {
target: {
value: '已有方向',
},
});
const input = document.querySelector<HTMLInputElement>('input[type="file"]');
expect(input).toBeTruthy();
fireEvent.change(input!, {
target: {
files: [new File(['unused'], '世界设定.md', { type: 'text/markdown' })],
},
});
await waitFor(() => {
expect(
(screen.getByPlaceholderText('输入消息') as HTMLTextAreaElement).value,
).toBe('已有方向\n\n第一章潮湿的港口');
});
});
test('creation agent workspace shows document parse error near composer', async () => {
ensureScrollApis();
vi.spyOn(
creationAgentServices,
'parseCreationAgentDocumentInput',
).mockRejectedValue(new Error('暂时只支持 txt、md、csv、json 文本文档。'));
render(
<CreationAgentWorkspace
session={{
sessionId: 'creation-agent-session-1',
title: null,
currentTurn: 0,
progressPercent: 0,
anchors: [],
messages: [],
}}
theme={testTheme}
loadingText="正在准备"
composerPlaceholder="输入消息"
primaryActionLabel="生成结果页"
onBack={() => {}}
onSubmitText={() => {}}
onPrimaryAction={() => {}}
/>,
);
const input = document.querySelector<HTMLInputElement>('input[type="file"]');
expect(input).toBeTruthy();
fireEvent.change(input!, {
target: {
files: [new File(['unused'], '世界设定.docx')],
},
});
await waitFor(() => {
expect(
screen.getByText('暂时只支持 txt、md、csv、json 文本文档。'),
).toBeTruthy();
});
});

View File

@@ -1,9 +1,11 @@
import { ArrowLeft, Send, Sparkles } from 'lucide-react';
import { ArrowLeft, Paperclip, Send, Sparkles } from 'lucide-react';
import type { ChangeEvent } from 'react';
import { useEffect, useRef, useState } from 'react';
import {
type CreationAgentProgressCopy,
normalizeCreationAgentProgress,
parseCreationAgentDocumentInput,
resolveCreationAgentProgressHint,
} from '../../services/creation-agent';
@@ -80,12 +82,13 @@ type CreationAgentWorkspaceProps = {
};
const AUTO_SCROLL_FOLLOW_THRESHOLD_PX = 96;
const DOCUMENT_INPUT_ACCEPT =
'.txt,.md,.markdown,.csv,.json,text/plain,text/markdown,text/csv,application/json';
function uniqueRecommendedReplies(recommendedReplies: string[] = []) {
return [...new Set(recommendedReplies.map((reply) => reply.trim()).filter(Boolean))].slice(
0,
3,
);
return [
...new Set(recommendedReplies.map((reply) => reply.trim()).filter(Boolean)),
].slice(0, 3);
}
function CreationAgentOperationBanner({
@@ -178,9 +181,8 @@ function CreationAgentMessageBubble({
}) {
const isUser = message.role === 'user';
const isSystem = message.role === 'system';
const visibleRecommendedReplies = isUser || isStreaming
? []
: uniqueRecommendedReplies(recommendedReplies);
const visibleRecommendedReplies =
isUser || isStreaming ? [] : uniqueRecommendedReplies(recommendedReplies);
const bubbleToneClass = isUser
? theme.userBubbleClass
: isSystem
@@ -201,7 +203,11 @@ function CreationAgentMessageBubble({
/>
</div>
) : (
<div className="flex items-center gap-1.5 py-1">
<div
data-testid="creation-agent-waiting-dots"
aria-label="等待回复"
className="flex items-center gap-1.5 py-1"
>
<span className="h-2 w-2 animate-pulse rounded-full bg-[var(--platform-text-soft)] [animation-delay:-0.2s]" />
<span className="h-2 w-2 animate-pulse rounded-full bg-[var(--platform-text-soft)] [animation-delay:-0.1s]" />
<span className="h-2 w-2 animate-pulse rounded-full bg-[var(--platform-text-soft)]" />
@@ -238,7 +244,10 @@ function shouldShowQuickAction(
return false;
}
if (typeof action.minTurn === 'number' && session.currentTurn < action.minTurn) {
if (
typeof action.minTurn === 'number' &&
session.currentTurn < action.minTurn
) {
return false;
}
@@ -287,8 +296,13 @@ export function CreationAgentWorkspace({
onQuickAction,
}: CreationAgentWorkspaceProps) {
const [draftText, setDraftText] = useState('');
const [documentInputError, setDocumentInputError] = useState<string | null>(
null,
);
const [isParsingDocumentInput, setIsParsingDocumentInput] = useState(false);
// 统一聊天区只在用户仍停留在底部附近时跟随新内容,避免流式回复持续抢走阅读位置。
const messageListRef = useRef<HTMLDivElement | null>(null);
const documentInputRef = useRef<HTMLInputElement | null>(null);
const shouldAutoScrollRef = useRef(true);
useEffect(() => {
@@ -298,7 +312,12 @@ export function CreationAgentWorkspace({
}
scrollMessageListToBottom(container);
}, [session?.sessionId, session?.messages, streamingReplyText, isStreamingReply]);
}, [
session?.sessionId,
session?.messages,
streamingReplyText,
isStreamingReply,
]);
if (!session) {
return (
@@ -318,7 +337,8 @@ export function CreationAgentWorkspace({
shouldShowQuickAction(action, session, progress),
);
const streamingMessageId = `streaming-assistant-${session.sessionId}`;
const shouldShowStreamingReply = isStreamingReply && streamingReplyText.trim();
// 用户消息提交后、首个流式文本到达前,也要立刻展示等待气泡。
const shouldShowStreamingReply = isStreamingReply;
const displayedMessages = shouldShowStreamingReply
? [
...session.messages,
@@ -356,18 +376,61 @@ export function CreationAgentWorkspace({
const submit = () => {
const text = draftText.trim();
if (!text || isBusy) {
if (!text || isBusy || isParsingDocumentInput) {
return;
}
armAutoScrollToBottom();
onSubmitText(text);
setDraftText('');
setDocumentInputError(null);
};
const appendDocumentInputText = (text: string) => {
setDraftText((current) => {
const currentText = current.trimEnd();
const nextText = text.trim();
return currentText ? `${currentText}\n\n${nextText}` : nextText;
});
};
const openDocumentInputPicker = () => {
documentInputRef.current?.click();
};
const handleDocumentInputChange = async (
event: ChangeEvent<HTMLInputElement>,
) => {
const file = event.target.files?.[0] ?? null;
event.target.value = '';
if (!file || isBusy || isParsingDocumentInput) {
return;
}
setIsParsingDocumentInput(true);
setDocumentInputError(null);
try {
const response = await parseCreationAgentDocumentInput(file);
appendDocumentInputText(response.document.text);
} catch (parseError) {
setDocumentInputError(
parseError instanceof Error
? parseError.message
: '解析文档失败,请重新选择文件。',
);
} finally {
setIsParsingDocumentInput(false);
}
};
return (
<div className="mx-auto flex h-full min-h-0 w-full max-w-6xl flex-col gap-3 overflow-hidden px-1 sm:px-0">
<div className={`relative overflow-hidden rounded-[1.8rem] px-4 py-4 text-white shadow-[0_20px_60px_rgba(15,23,42,0.18)] sm:px-5 ${theme.heroClass}`}>
<div
className={`relative overflow-hidden rounded-[1.8rem] px-4 py-4 text-white shadow-[0_20px_60px_rgba(15,23,42,0.18)] sm:px-5 ${theme.heroClass}`}
>
<div className="flex items-start justify-between gap-3">
<button
type="button"
@@ -482,20 +545,43 @@ export function CreationAgentWorkspace({
)}
</div>
{error ? (
{documentInputError || error ? (
<div className="mx-4 mb-3 rounded-[1rem] border border-red-200/70 bg-red-50 px-3 py-2 text-sm text-red-600">
{error}
{documentInputError || error}
</div>
) : null}
<div className="border-t border-[var(--platform-subpanel-border)] p-3">
<div className="flex items-end gap-2 rounded-[1.25rem] bg-white/80 p-2">
<input
ref={documentInputRef}
type="file"
accept={DOCUMENT_INPUT_ACCEPT}
className="hidden"
onChange={handleDocumentInputChange}
/>
<button
type="button"
aria-label={
isParsingDocumentInput ? '正在解析文档' : '上传文档'
}
title={isParsingDocumentInput ? '正在解析文档' : '上传文档'}
aria-busy={isParsingDocumentInput}
disabled={isBusy || isParsingDocumentInput}
onClick={openDocumentInputPicker}
className="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-full text-[var(--platform-text-base)] hover:bg-black/5 disabled:cursor-not-allowed disabled:opacity-40"
>
<Paperclip
className={`h-4 w-4 ${isParsingDocumentInput ? 'animate-pulse' : ''}`}
/>
</button>
<textarea
value={draftText}
disabled={isBusy}
disabled={isBusy || isParsingDocumentInput}
rows={2}
onChange={(event) => {
setDraftText(event.target.value);
setDocumentInputError(null);
}}
onKeyDown={(event) => {
if (event.key === 'Enter' && !event.shiftKey) {
@@ -509,7 +595,7 @@ export function CreationAgentWorkspace({
<button
type="button"
aria-label="发送"
disabled={isBusy || !draftText.trim()}
disabled={isBusy || isParsingDocumentInput || !draftText.trim()}
onClick={submit}
className={`inline-flex h-11 w-11 items-center justify-center rounded-full text-white disabled:cursor-not-allowed disabled:opacity-40 ${theme.userBubbleClass}`}
>

View File

@@ -4,15 +4,16 @@ import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contra
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
import { CustomWorldCreationStartCard } from './CustomWorldCreationStartCard';
import {
CustomWorldWorkCard,
type UnifiedCreationWorkItem,
} from './CustomWorldWorkCard';
import { CustomWorldWorkCard } from './CustomWorldWorkCard';
import {
type CustomWorldWorkFilter,
CustomWorldWorkTabs,
} from './CustomWorldWorkTabs';
import type { PlatformCreationTypeId } from '../platform-entry/platformEntryCreationTypes';
import {
buildCreationWorkShelfItems,
type CreationWorkShelfItem,
} from './creationWorkShelf';
type CustomWorldCreationHubProps = {
items: CustomWorldWorkSummary[];
@@ -71,42 +72,111 @@ export function CustomWorldCreationHub({
}: CustomWorldCreationHubProps) {
const [activeFilter, setActiveFilter] =
useState<CustomWorldWorkFilter>('all');
const unifiedItems = useMemo<UnifiedCreationWorkItem[]>(
() => [
...items.map((item) => ({ kind: 'rpg', item }) as const),
...bigFishItems.map((item) => ({ kind: 'big-fish', item }) as const),
...puzzleItems.map((item) => ({ kind: 'puzzle', item }) as const),
const shelfItems = useMemo(
() =>
buildCreationWorkShelfItems({
rpgItems: items,
bigFishItems,
puzzleItems,
canDeleteRpg: Boolean(onDeletePublished),
canDeleteBigFish: Boolean(onDeleteBigFish),
canDeletePuzzle: Boolean(onDeletePuzzle),
}),
[
bigFishItems,
items,
onDeleteBigFish,
onDeletePublished,
onDeletePuzzle,
puzzleItems,
],
[bigFishItems, items, puzzleItems],
);
const draftCount = unifiedItems.filter((entry) =>
entry.kind === 'puzzle'
? entry.item.publicationStatus === 'draft'
: entry.kind === 'big-fish'
? entry.item.status === 'draft'
: entry.item.status === 'draft',
).length;
const publishedCount = unifiedItems.filter((entry) =>
entry.kind === 'puzzle'
? entry.item.publicationStatus === 'published'
: entry.kind === 'big-fish'
? entry.item.status === 'published'
: entry.item.status === 'published',
const draftCount = shelfItems.filter((entry) => entry.status === 'draft').length;
const publishedCount = shelfItems.filter(
(entry) => entry.status === 'published',
).length;
const filteredItems = useMemo(
() =>
unifiedItems.filter((entry) =>
activeFilter === 'all'
? true
: entry.kind === 'puzzle'
? entry.item.publicationStatus === activeFilter
: entry.kind === 'big-fish'
? entry.item.status === activeFilter
: entry.item.status === activeFilter,
shelfItems.filter((entry) =>
activeFilter === 'all' ? true : entry.status === activeFilter,
),
[activeFilter, unifiedItems],
[activeFilter, shelfItems],
);
function handleOpenShelfItem(item: CreationWorkShelfItem) {
switch (item.source.kind) {
case 'puzzle':
onOpenPuzzleDetail?.(item.source.item);
return;
case 'big-fish':
onOpenBigFishDetail?.(item.source.item);
return;
case 'rpg':
if (item.status === 'draft') {
onOpenDraft(item.source.item);
return;
}
if (item.source.item.profileId) {
onEnterPublished(item.source.item.profileId);
}
}
}
function buildExperienceAction(item: CreationWorkShelfItem) {
if (!item.canExperience) {
return null;
}
switch (item.source.kind) {
case 'puzzle': {
const sourceItem = item.source.item;
return () => {
onExperiencePuzzle?.(sourceItem.profileId);
};
}
case 'big-fish': {
const sourceItem = item.source.item;
return () => {
onExperienceBigFish?.(sourceItem);
};
}
case 'rpg': {
const sourceItem = item.source.item;
return () => {
onExperienceRpg?.(sourceItem);
};
}
}
}
function buildDeleteAction(item: CreationWorkShelfItem) {
if (!item.canDelete) {
return null;
}
switch (item.source.kind) {
case 'puzzle': {
const sourceItem = item.source.item;
return () => {
onDeletePuzzle?.(sourceItem);
};
}
case 'big-fish': {
const sourceItem = item.source.item;
return () => {
onDeleteBigFish?.(sourceItem);
};
}
case 'rpg': {
const sourceItem = item.source.item;
return () => {
onDeletePublished?.(sourceItem);
};
}
}
}
return (
<div className="platform-page-stage platform-remap-surface space-y-4 px-3 pb-4 pt-3 sm:px-4 sm:pt-4 xl:px-5 xl:pb-5 xl:pt-5">
<div className="space-y-4 xl:space-y-3">
@@ -158,65 +228,16 @@ export function CustomWorldCreationHub({
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
{filteredItems.map((item) => (
<CustomWorldWorkCard
key={`${item.kind}-${item.item.workId}`}
key={`${item.kind}-${item.id}`}
item={item}
onOpen={() => {
if (item.kind === 'puzzle') {
onOpenPuzzleDetail?.(item.item);
return;
}
if (item.kind === 'big-fish') {
onOpenBigFishDetail?.(item.item);
return;
}
if (item.item.status === 'draft') {
onOpenDraft(item.item);
return;
}
if (item.item.profileId) {
onEnterPublished(item.item.profileId);
}
}}
onExperience={
item.kind === 'puzzle'
? item.item.publicationStatus === 'published'
? () => {
onExperiencePuzzle?.(item.item.profileId);
}
: null
: item.kind === 'big-fish'
? item.item.status === 'published'
? () => {
onExperienceBigFish?.(item.item);
}
: null
: item.item.status === 'published' && item.item.canEnterWorld
? () => {
onExperienceRpg?.(item.item);
}
: null
}
onDelete={
item.kind === 'puzzle'
? () => {
onDeletePuzzle?.(item.item);
}
: item.kind === 'big-fish'
? () => {
onDeleteBigFish?.(item.item);
}
: () => {
onDeletePublished?.(item.item);
}
}
deleteBusy={deletingWorkId === item.item.workId}
onOpen={() => handleOpenShelfItem(item)}
onExperience={buildExperienceAction(item)}
onDelete={buildDeleteAction(item)}
deleteBusy={deletingWorkId === item.id}
/>
))}
</div>
) : unifiedItems.length === 0 ? (
) : shelfItems.length === 0 ? (
<EmptyState title="还没有作品" />
) : (
<EmptyState title="当前筛选下没有内容" />

View File

@@ -1,7 +1,5 @@
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldAgent';
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
import { CustomWorldCoverArtwork } from '../CustomWorldCoverArtwork';
import type { CreationWorkShelfItem } from './creationWorkShelf';
function formatUpdatedAt(value: string) {
const date = new Date(value);
@@ -17,28 +15,20 @@ function formatUpdatedAt(value: string) {
}).format(date);
}
export type UnifiedCreationWorkItem =
| {
kind: 'rpg';
item: CustomWorldWorkSummary;
}
| {
kind: 'big-fish';
item: BigFishWorkSummary;
}
| {
kind: 'puzzle';
item: PuzzleWorkSummary;
};
type CustomWorldWorkCardProps = {
item: UnifiedCreationWorkItem;
item: CreationWorkShelfItem;
onOpen: () => void;
onExperience?: (() => void) | null;
onDelete?: (() => void) | null;
deleteBusy?: boolean;
};
const BADGE_TONE_CLASS: Record<CreationWorkShelfItem['badges'][number]['tone'], string> = {
warm: 'platform-pill--warm',
success: 'platform-pill--success',
neutral: 'platform-pill--neutral',
};
export function CustomWorldWorkCard({
item,
onOpen,
@@ -46,40 +36,11 @@ export function CustomWorldWorkCard({
onDelete = null,
deleteBusy = false,
}: CustomWorldWorkCardProps) {
const isPuzzle = item.kind === 'puzzle';
const isBigFish = item.kind === 'big-fish';
const isDraft =
item.kind === 'puzzle'
? item.item.publicationStatus === 'draft'
: item.kind === 'big-fish'
? item.item.status === 'draft'
: item.item.status === 'draft';
const openActionLabel = isPuzzle || isBigFish
? isDraft
? '继续创作'
: '查看详情'
: isDraft
? item.item.playableNpcCount > 0 || item.item.landmarkCount > 0
? '继续完善'
: '继续创作'
: '查看详情';
const title =
item.kind === 'puzzle' ? item.item.levelName : item.item.title;
const subtitle =
item.kind === 'puzzle' ? item.item.authorDisplayName : item.item.subtitle;
const summary = item.item.summary;
const updatedAt = item.item.updatedAt;
const coverImageSrc = item.item.coverImageSrc ?? null;
const coverRenderMode =
item.kind === 'rpg' ? item.item.coverRenderMode : 'image';
const coverCharacterImageSrcs =
item.kind === 'rpg' ? item.item.coverCharacterImageSrcs : [];
return (
<div
role="button"
tabIndex={0}
aria-label={`${openActionLabel}${title}`}
aria-label={`${item.openActionLabel}${item.title}`}
onClick={onOpen}
onKeyDown={(event) => {
if (event.key !== 'Enter' && event.key !== ' ') {
@@ -92,48 +53,29 @@ export function CustomWorldWorkCard({
className="platform-surface platform-interactive-card relative min-h-[16rem] cursor-pointer overflow-hidden px-4 py-4 text-left sm:min-h-[15.5rem] xl:min-h-[14rem] xl:px-4 xl:py-3.5"
>
<CustomWorldCoverArtwork
imageSrc={coverImageSrc}
title={title}
imageSrc={item.coverImageSrc}
title={item.title}
fallbackLabel="封面"
renderMode={coverRenderMode}
characterImageSrcs={coverCharacterImageSrcs}
renderMode={item.coverRenderMode}
characterImageSrcs={item.coverCharacterImageSrcs}
className="platform-cover-artwork absolute inset-0"
/>
<div className="absolute inset-0 bg-[var(--platform-card-overlay-strong)]" />
<div className="pointer-events-none relative z-20 flex min-h-[14rem] flex-col sm:min-h-[13.5rem] xl:min-h-[12.5rem]">
<div className="flex items-start justify-between gap-3">
<div className="flex flex-wrap gap-2">
<span
className={`platform-pill px-3 py-1 text-[10px] ${
isDraft
? 'platform-pill--warm'
: 'platform-pill--success'
}`}
>
{isDraft ? '草稿' : '已发布'}
</span>
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">
{isPuzzle ? '拼图' : isBigFish ? '大鱼' : 'RPG'}
</span>
{item.kind === 'rpg' && item.item.stageLabel ? (
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">
{item.item.stageLabel}
{item.badges.map((badge) => (
<span
key={`${item.id}-${badge.id}`}
className={`platform-pill ${BADGE_TONE_CLASS[badge.tone]} px-3 py-1 text-[10px]`}
>
{badge.label}
</span>
) : null}
{isPuzzle
? item.item.themeTags.slice(0, 2).map((tag) => (
<span
key={`${item.item.profileId}-${tag}`}
className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]"
>
{tag}
</span>
))
: null}
))}
</div>
<div className="flex shrink-0 items-center gap-2">
<span className="text-[11px] text-[var(--platform-text-soft)]">
{formatUpdatedAt(updatedAt)}
{formatUpdatedAt(item.updatedAt)}
</span>
{onDelete ? (
<button
@@ -174,69 +116,26 @@ export function CustomWorldWorkCard({
<div className="mt-4 min-h-0 xl:mt-3">
<div className="text-2xl font-black text-[var(--platform-text-strong)] xl:text-xl">
{title}
{item.title}
</div>
<div className="mt-1 text-xs tracking-[0.12em] text-[var(--platform-text-soft)]">
{subtitle}
{item.subtitle}
</div>
<div className="mt-3 line-clamp-2 text-sm leading-6 text-[color:color-mix(in_srgb,var(--platform-text-base)_92%,transparent)] xl:mt-2">
{summary}
{item.summary}
</div>
</div>
<div className="mt-auto flex flex-col gap-3 pt-4 sm:flex-row sm:items-end sm:justify-between xl:gap-2 xl:pt-3">
<div className="flex flex-wrap gap-2">
{isPuzzle ? (
<>
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">
{item.item.authorDisplayName}
</span>
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">
{item.item.playCount}
</span>
</>
) : isBigFish ? (
<>
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">
{item.item.levelCount}
</span>
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">
{item.item.levelMainImageReadyCount}
</span>
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">
{item.item.levelMotionReadyCount}
</span>
{item.item.backgroundReady ? (
<span className="platform-pill platform-pill--success px-3 py-1 text-[10px]">
</span>
) : null}
</>
) : (
<>
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">
{isDraft ? '角色' : '可扮演角色'} {item.item.playableNpcCount}
</span>
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">
{item.item.landmarkCount}
</span>
{item.item.roleVisualReadyCount ? (
<span className="platform-pill platform-pill--warm px-3 py-1 text-[10px]">
{item.item.roleVisualReadyCount}
</span>
) : null}
{item.item.roleAnimationReadyCount ? (
<span className="platform-pill platform-pill--success px-3 py-1 text-[10px]">
{item.item.roleAnimationReadyCount}
</span>
) : null}
{item.item.roleAssetSummaryLabel ? (
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">
{item.item.roleAssetSummaryLabel}
</span>
) : null}
</>
)}
{item.metrics.map((metric) => (
<span
key={`${item.id}-${metric.id}`}
className={`platform-pill ${BADGE_TONE_CLASS[metric.tone ?? 'neutral']} px-3 py-1 text-[10px]`}
>
{metric.label}
</span>
))}
</div>
<div className="flex flex-wrap gap-2 sm:justify-end xl:gap-1.5">
{onExperience ? (

View File

@@ -0,0 +1,247 @@
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldAgent';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
export type CreationWorkShelfKind = 'rpg' | 'big-fish' | 'puzzle';
export type CreationWorkShelfStatus = 'draft' | 'published';
export type CreationWorkShelfBadgeTone = 'warm' | 'success' | 'neutral';
export type CreationWorkShelfBadge = {
id: string;
label: string;
tone: CreationWorkShelfBadgeTone;
};
export type CreationWorkShelfMetric = {
id: string;
label: string;
tone?: CreationWorkShelfBadgeTone;
};
export type CreationWorkShelfSource =
| {
kind: 'rpg';
item: CustomWorldWorkSummary;
}
| {
kind: 'big-fish';
item: BigFishWorkSummary;
}
| {
kind: 'puzzle';
item: PuzzleWorkSummary;
};
export type CreationWorkShelfItem = {
id: string;
kind: CreationWorkShelfKind;
status: CreationWorkShelfStatus;
title: string;
subtitle: string;
summary: string;
updatedAt: string;
coverImageSrc: string | null;
coverRenderMode: 'image' | 'scene_with_roles';
coverCharacterImageSrcs: string[];
typeLabel: string;
openActionLabel: string;
canExperience: boolean;
canDelete: boolean;
badges: CreationWorkShelfBadge[];
metrics: CreationWorkShelfMetric[];
source: CreationWorkShelfSource;
};
export function buildCreationWorkShelfItems(params: {
rpgItems: CustomWorldWorkSummary[];
bigFishItems: BigFishWorkSummary[];
puzzleItems: PuzzleWorkSummary[];
canDeleteRpg?: boolean;
canDeleteBigFish?: boolean;
canDeletePuzzle?: boolean;
}) {
const {
rpgItems,
bigFishItems,
puzzleItems,
canDeleteRpg = false,
canDeleteBigFish = false,
canDeletePuzzle = false,
} = params;
return [
...rpgItems.map((item) => mapRpgWorkToShelfItem(item, canDeleteRpg)),
...bigFishItems.map((item) =>
mapBigFishWorkToShelfItem(item, canDeleteBigFish),
),
...puzzleItems.map((item) => mapPuzzleWorkToShelfItem(item, canDeletePuzzle)),
].sort(
(left, right) =>
getShelfItemTime(right.updatedAt) - getShelfItemTime(left.updatedAt),
);
}
function mapRpgWorkToShelfItem(
item: CustomWorldWorkSummary,
canDelete: boolean,
): CreationWorkShelfItem {
const isDraft = item.status === 'draft';
const badges: CreationWorkShelfBadge[] = [
buildStatusBadge(item.status),
{ id: 'type', label: 'RPG', tone: 'neutral' },
];
if (item.stageLabel) {
badges.push({ id: 'stage', label: item.stageLabel, tone: 'neutral' });
}
const metrics: CreationWorkShelfMetric[] = [
{
id: 'playable-npc-count',
label: `${isDraft ? '角色' : '可扮演角色'} ${item.playableNpcCount}`,
},
{ id: 'landmark-count', label: `地点 ${item.landmarkCount}` },
];
if (item.roleVisualReadyCount) {
metrics.push({
id: 'role-visual-ready-count',
label: `主图 ${item.roleVisualReadyCount}`,
tone: 'warm',
});
}
if (item.roleAnimationReadyCount) {
metrics.push({
id: 'role-animation-ready-count',
label: `动作 ${item.roleAnimationReadyCount}`,
tone: 'success',
});
}
if (item.roleAssetSummaryLabel) {
metrics.push({
id: 'role-asset-summary',
label: item.roleAssetSummaryLabel,
});
}
return {
id: item.workId,
kind: 'rpg',
status: item.status,
title: item.title,
subtitle: item.subtitle,
summary: item.summary,
updatedAt: item.updatedAt,
coverImageSrc: item.coverImageSrc ?? null,
coverRenderMode: item.coverRenderMode ?? 'image',
coverCharacterImageSrcs: item.coverCharacterImageSrcs ?? [],
typeLabel: 'RPG',
openActionLabel: isDraft
? item.playableNpcCount > 0 || item.landmarkCount > 0
? '继续完善'
: '继续创作'
: '查看详情',
canExperience: item.status === 'published' && item.canEnterWorld,
canDelete,
badges,
metrics,
source: { kind: 'rpg', item },
};
}
function mapBigFishWorkToShelfItem(
item: BigFishWorkSummary,
canDelete: boolean,
): CreationWorkShelfItem {
return {
id: item.workId,
kind: 'big-fish',
status: item.status,
title: item.title,
subtitle: item.subtitle,
summary: item.summary,
updatedAt: item.updatedAt,
coverImageSrc: item.coverImageSrc ?? null,
coverRenderMode: 'image',
coverCharacterImageSrcs: [],
typeLabel: '大鱼',
openActionLabel: item.status === 'draft' ? '继续创作' : '查看详情',
canExperience: item.status === 'published',
canDelete,
badges: [
buildStatusBadge(item.status),
{ id: 'type', label: '大鱼', tone: 'neutral' },
],
metrics: [
{ id: 'level-count', label: `关卡 ${item.levelCount}` },
{
id: 'level-main-image-ready-count',
label: `主图 ${item.levelMainImageReadyCount}`,
},
{
id: 'level-motion-ready-count',
label: `动作 ${item.levelMotionReadyCount}`,
},
...(item.backgroundReady
? [
{
id: 'background-ready',
label: '背景已就绪',
tone: 'success' as const,
},
]
: []),
],
source: { kind: 'big-fish', item },
};
}
function mapPuzzleWorkToShelfItem(
item: PuzzleWorkSummary,
canDelete: boolean,
): CreationWorkShelfItem {
const status = item.publicationStatus;
return {
id: item.workId,
kind: 'puzzle',
status,
title: item.levelName,
subtitle: item.authorDisplayName,
summary: item.summary,
updatedAt: item.updatedAt,
coverImageSrc: item.coverImageSrc ?? null,
coverRenderMode: 'image',
coverCharacterImageSrcs: [],
typeLabel: '拼图',
openActionLabel: status === 'draft' ? '继续创作' : '查看详情',
canExperience: status === 'published',
canDelete,
badges: [
buildStatusBadge(status),
{ id: 'type', label: '拼图', tone: 'neutral' },
...item.themeTags.slice(0, 2).map((tag) => ({
id: `tag:${tag}`,
label: tag,
tone: 'neutral' as const,
})),
],
metrics: [
{ id: 'author', label: `作者 ${item.authorDisplayName}` },
{ id: 'play-count', label: `游玩 ${item.playCount}` },
],
source: { kind: 'puzzle', item },
};
}
function buildStatusBadge(status: CreationWorkShelfStatus): CreationWorkShelfBadge {
return {
id: 'status',
label: status === 'draft' ? '草稿' : '已发布',
tone: status === 'draft' ? 'warm' : 'success',
};
}
function getShelfItemTime(value: string) {
const timestamp = new Date(value).getTime();
return Number.isNaN(timestamp) ? 0 : timestamp;
}

View File

@@ -59,7 +59,7 @@ import {
getPuzzleAgentSession,
streamPuzzleAgentMessage,
} from '../../services/puzzle-agent';
import { getPuzzleGalleryDetail } from '../../services/puzzle-gallery';
import { getPuzzleGalleryDetail, listPuzzleGallery } from '../../services/puzzle-gallery';
import {
advanceLocalPuzzleLevel,
dragLocalPuzzlePiece,
@@ -82,6 +82,11 @@ import { useRpgCreationAgentOperationPolling } from '../rpg-entry/useRpgCreation
import { useRpgCreationEnterWorld } from '../rpg-entry/useRpgCreationEnterWorld';
import { useRpgCreationResultAutosave } from '../rpg-entry/useRpgCreationResultAutosave';
import { useRpgCreationSessionController } from '../rpg-entry/useRpgCreationSessionController';
import {
isPuzzleGalleryEntry,
mapPuzzleWorkToPlatformGalleryCard,
type PlatformPublicGalleryCard,
} from '../rpg-entry/rpgEntryWorldPresentation';
import { PlatformEntryCreationTypeModal } from './PlatformEntryCreationTypeModal';
import { PlatformEntryHomeView } from './PlatformEntryHomeView';
import {
@@ -93,6 +98,7 @@ import type { PlatformCreationTypeId } from './platformEntryCreationTypes';
import type { PlatformEntryFlowShellProps } from './platformEntryTypes';
import { PlatformEntryWorldDetailView } from './PlatformEntryWorldDetailView';
import { usePlatformEntryBootstrap } from './usePlatformEntryBootstrap';
import { usePlatformCreationAgentFlowController } from './usePlatformCreationAgentFlowController';
import { usePlatformEntryLibraryDetail } from './usePlatformEntryLibraryDetail';
import { usePlatformEntryNavigation } from './usePlatformEntryNavigation';
@@ -114,6 +120,33 @@ const AGENT_RESULT_STRUCTURAL_BLOCKER_CODES = new Set([
'publish_missing_first_act',
]);
function getPlatformPublicGalleryEntryTime(entry: PlatformPublicGalleryCard) {
const rawTime = entry.publishedAt ?? entry.updatedAt;
const timestamp = new Date(rawTime).getTime();
return Number.isNaN(timestamp) ? 0 : timestamp;
}
function getPlatformPublicGalleryEntryKey(entry: PlatformPublicGalleryCard) {
return `${isPuzzleGalleryEntry(entry) ? 'puzzle' : 'rpg'}:${entry.ownerUserId}:${entry.profileId}`;
}
function mergePlatformPublicGalleryEntries(
rpgEntries: CustomWorldGalleryCard[],
puzzleEntries: PlatformPublicGalleryCard[],
) {
const entryMap = new Map<string, PlatformPublicGalleryCard>();
[...rpgEntries, ...puzzleEntries].forEach((entry) => {
entryMap.set(getPlatformPublicGalleryEntryKey(entry), entry);
});
return Array.from(entryMap.values()).sort(
(left, right) =>
getPlatformPublicGalleryEntryTime(right) -
getPlatformPublicGalleryEntryTime(left),
);
}
function readProfileTextField(
profile: CustomWorldProfile | null,
paths: string[],
@@ -304,28 +337,20 @@ export function PlatformEntryFlowShellImpl({
const [showCreationTypeModal, setShowCreationTypeModal] = useState(false);
const [selectedDetailEntry, setSelectedDetailEntry] =
useState<CustomWorldLibraryEntry<CustomWorldProfile> | null>(null);
const [bigFishSession, setBigFishSession] =
useState<BigFishSessionSnapshotResponse | null>(null);
const [bigFishWorks, setBigFishWorks] = useState<BigFishWorkSummary[]>([]);
const [bigFishRun, setBigFishRun] =
useState<BigFishRuntimeSnapshotResponse | null>(null);
const [bigFishError, setBigFishError] = useState<string | null>(null);
const [isBigFishBusy, setIsBigFishBusy] = useState(false);
const [isBigFishLoadingLibrary, setIsBigFishLoadingLibrary] = useState(false);
const [streamingBigFishReplyText, setStreamingBigFishReplyText] =
useState('');
const [isStreamingBigFishReply, setIsStreamingBigFishReply] = useState(false);
const bigFishInputInFlightRef = useRef(false);
const [puzzleSession, setPuzzleSession] =
useState<PuzzleAgentSessionSnapshot | null>(null);
const [puzzleOperation, setPuzzleOperation] =
useState<PuzzleAgentOperationRecord | null>(null);
const [puzzleWorks, setPuzzleWorks] = useState<PuzzleWorkSummary[]>([]);
const [puzzleGalleryEntries, setPuzzleGalleryEntries] = useState<
PuzzleWorkSummary[]
>([]);
const [selectedPuzzleDetail, setSelectedPuzzleDetail] =
useState<PuzzleWorkSummary | null>(null);
const [puzzleRun, setPuzzleRun] = useState<PuzzleRunSnapshot | null>(null);
const [puzzleError, setPuzzleError] = useState<string | null>(null);
const [isPuzzleBusy, setIsPuzzleBusy] = useState(false);
const [isPuzzleLoadingLibrary, setIsPuzzleLoadingLibrary] = useState(false);
const [isSearchingPublicCode, setIsSearchingPublicCode] = useState(false);
const [publicSearchError, setPublicSearchError] = useState<string | null>(null);
@@ -334,8 +359,6 @@ export function PlatformEntryFlowShellImpl({
const [deletingCreationWorkId, setDeletingCreationWorkId] = useState<
string | null
>(null);
const [streamingPuzzleReplyText, setStreamingPuzzleReplyText] = useState('');
const [isStreamingPuzzleReply, setIsStreamingPuzzleReply] = useState(false);
const hasInitialAgentSession = Boolean(
readCustomWorldAgentUiState().activeSessionId,
);
@@ -359,6 +382,17 @@ export function PlatformEntryFlowShellImpl({
setPlatformTab('create');
}, [setPlatformTab]);
const resolveBigFishErrorMessage = useCallback(
(error: unknown, fallback: string) =>
resolveRpgCreationErrorMessage(error, fallback),
[],
);
const resolvePuzzleErrorMessage = useCallback(
(error: unknown, fallback: string) =>
resolveRpgCreationErrorMessage(error, fallback),
[],
);
const sessionController = useRpgCreationSessionController({
userId: authUi?.user?.id,
openLoginModal: authUi?.openLoginModal,
@@ -441,10 +475,18 @@ export function PlatformEntryFlowShellImpl({
agentSessionProfile: sessionController.agentDraftResultProfile,
agentSession: sessionController.agentSession,
handleCustomWorldSelect,
executePublishWorld: () =>
autosaveCoordinator.executeAgentActionAndWait({
executePublishWorld: async () => {
const latestSession = await autosaveCoordinator.executeAgentActionAndWait({
action: 'publish_world',
}),
});
// 发布动作会在后端同步 gallery 投影;前端发布完成后立即刷新首页/分类页共用的公开作品列表。
await Promise.allSettled([
platformBootstrap.refreshPublishedGallery(),
platformBootstrap.refreshCustomWorldWorks(),
refreshPuzzleGallery(),
]);
return latestSession;
},
setGeneratedCustomWorldProfile:
sessionController.setGeneratedCustomWorldProfile,
});
@@ -495,8 +537,24 @@ export function PlatformEntryFlowShellImpl({
}, [agentResultPreview]);
const featuredGalleryEntries = useMemo(
() => platformBootstrap.publishedGalleryEntries.slice(0, 6),
[platformBootstrap.publishedGalleryEntries],
() => {
const puzzlePublicEntries = puzzleGalleryEntries.map(
mapPuzzleWorkToPlatformGalleryCard,
);
return mergePlatformPublicGalleryEntries(
platformBootstrap.publishedGalleryEntries,
puzzlePublicEntries,
).slice(0, 6);
},
[platformBootstrap.publishedGalleryEntries, puzzleGalleryEntries],
);
const latestGalleryEntries = useMemo(
() =>
mergePlatformPublicGalleryEntries(
platformBootstrap.publishedGalleryEntries,
puzzleGalleryEntries.map(mapPuzzleWorkToPlatformGalleryCard),
),
[platformBootstrap.publishedGalleryEntries, puzzleGalleryEntries],
);
const creationHubItems =
@@ -523,17 +581,6 @@ export function PlatformEntryFlowShellImpl({
setSelectionStage,
]);
useEffect(() => {
if (selectionStage === 'big-fish-result' && !bigFishSession?.draft) {
setSelectionStage(
bigFishSession ? 'big-fish-agent-workspace' : 'platform',
);
}
if (selectionStage === 'big-fish-runtime' && !bigFishRun) {
setSelectionStage(bigFishSession?.draft ? 'big-fish-result' : 'platform');
}
}, [bigFishRun, bigFishSession, selectionStage, setSelectionStage]);
const runProtectedAction = useCallback(
(action: () => void) => {
if (!authUi?.requireAuth) {
@@ -647,17 +694,6 @@ export function PlatformEntryFlowShellImpl({
setShowCreationTypeModal(true);
}, [prepareCreationLaunch]);
const resolveBigFishErrorMessage = useCallback(
(error: unknown, fallback: string) =>
resolveRpgCreationErrorMessage(error, fallback),
[],
);
const resolvePuzzleErrorMessage = useCallback(
(error: unknown, fallback: string) =>
resolveRpgCreationErrorMessage(error, fallback),
[],
);
const refreshBigFishShelf = useCallback(async () => {
setIsBigFishLoadingLibrary(true);
@@ -690,64 +726,144 @@ export function PlatformEntryFlowShellImpl({
}
}, [resolvePuzzleErrorMessage]);
const openBigFishAgentWorkspace = useCallback(async () => {
if (isBigFishBusy) {
return;
}
setIsBigFishBusy(true);
setBigFishError(null);
setBigFishRun(null);
const refreshPuzzleGallery = useCallback(async () => {
try {
const { session } = await createBigFishCreationSession({});
setBigFishSession(session);
enterCreateTab();
setShowCreationTypeModal(false);
setSelectionStage('big-fish-agent-workspace');
const galleryResponse = await listPuzzleGallery();
setPuzzleGalleryEntries(galleryResponse.items);
return galleryResponse.items;
} catch (error) {
setBigFishError(
resolveBigFishErrorMessage(error, '开启大鱼吃小鱼共创工作台失败。'),
setPuzzleGalleryEntries([]);
setPuzzleError(
resolvePuzzleErrorMessage(error, '读取拼图广场失败。'),
);
} finally {
setIsBigFishBusy(false);
return [];
}
}, [
}, [resolvePuzzleErrorMessage]);
const bigFishFlow = usePlatformCreationAgentFlowController<
BigFishSessionSnapshotResponse,
Record<string, never>,
{ session: BigFishSessionSnapshotResponse },
SendBigFishMessageRequest,
ExecuteBigFishActionRequest,
{ session: BigFishSessionSnapshotResponse }
>({
client: {
createSession: createBigFishCreationSession,
getSession: getBigFishCreationSession,
streamMessage: streamBigFishCreationMessage,
executeAction: executeBigFishCreationAction,
selectSession: (response) => response.session,
},
createPayload: {},
workspaceStage: 'big-fish-agent-workspace',
resultStage: 'big-fish-result',
platformStage: 'platform',
isCompileAction: (payload) => payload.action === 'big_fish_compile_draft',
resolveErrorMessage: resolveBigFishErrorMessage,
errorMessages: {
open: '开启大鱼吃小鱼共创工作台失败。',
restoreMissingSession: '这份大鱼吃小鱼草稿缺少会话信息,请重新开始创作。',
restore: '读取大鱼吃小鱼创作草稿失败。',
submit: '发送大鱼吃小鱼共创消息失败。',
execute: '执行大鱼吃小鱼操作失败。',
},
enterCreateTab,
isBigFishBusy,
resolveBigFishErrorMessage,
setSelectionStage,
]);
onSessionOpened: () => {
setShowCreationTypeModal(false);
},
onActionComplete: ({ response, setSession }) => {
setSession(response.session);
},
});
const puzzleFlow = usePlatformCreationAgentFlowController<
PuzzleAgentSessionSnapshot,
Record<string, never>,
{ session: PuzzleAgentSessionSnapshot },
SendPuzzleAgentMessageRequest,
PuzzleAgentActionRequest,
{ operation: PuzzleAgentOperationRecord }
>({
client: {
createSession: createPuzzleAgentSession,
getSession: getPuzzleAgentSession,
streamMessage: streamPuzzleAgentMessage,
executeAction: executePuzzleAgentAction,
selectSession: (response) => response.session,
},
createPayload: {},
workspaceStage: 'puzzle-agent-workspace',
resultStage: 'puzzle-result',
platformStage: 'platform',
isCompileAction: (payload) => payload.action === 'compile_puzzle_draft',
resolveErrorMessage: resolvePuzzleErrorMessage,
errorMessages: {
open: '开启拼图共创工作台失败。',
restoreMissingSession: '这份拼图草稿缺少会话信息,请重新开始创作。',
restore: '读取拼图创作草稿失败。',
submit: '发送拼图共创消息失败。',
execute: '执行拼图操作失败。',
},
enterCreateTab,
setSelectionStage,
onSessionOpened: () => {
setShowCreationTypeModal(false);
},
onActionComplete: async ({ payload, response, session, setSession }) => {
setPuzzleOperation(response.operation);
if (payload.action === 'publish_puzzle_work') {
await Promise.allSettled([
refreshPuzzleShelf(),
refreshPuzzleGallery(),
]);
}
const latestResponse = await getPuzzleAgentSession(session.sessionId);
const latestSession = latestResponse.session;
setSession(latestSession);
if (
payload.action === 'publish_puzzle_work' &&
latestSession.publishedProfileId
) {
const galleryDetail = await getPuzzleGalleryDetail(
latestSession.publishedProfileId,
);
setSelectedPuzzleDetail(galleryDetail.item);
setSelectionStage('puzzle-gallery-detail');
}
},
});
const bigFishSession = bigFishFlow.session;
const bigFishError = bigFishFlow.error;
const setBigFishError = bigFishFlow.setError;
const isBigFishBusy = bigFishFlow.isBusy;
const setIsBigFishBusy = bigFishFlow.setIsBusy;
const streamingBigFishReplyText = bigFishFlow.streamingReplyText;
const isStreamingBigFishReply = bigFishFlow.isStreamingReply;
const puzzleSession = puzzleFlow.session;
const puzzleError = puzzleFlow.error;
const setPuzzleError = puzzleFlow.setError;
const isPuzzleBusy = puzzleFlow.isBusy;
const setIsPuzzleBusy = puzzleFlow.setIsBusy;
const streamingPuzzleReplyText = puzzleFlow.streamingReplyText;
const isStreamingPuzzleReply = puzzleFlow.isStreamingReply;
const openBigFishAgentWorkspace = useCallback(async () => {
setBigFishRun(null);
await bigFishFlow.openWorkspace();
}, [bigFishFlow]);
const openPuzzleAgentWorkspace = useCallback(async () => {
if (isPuzzleBusy) {
return;
}
setIsPuzzleBusy(true);
setPuzzleError(null);
setPuzzleRun(null);
setPuzzleOperation(null);
try {
const { session } = await createPuzzleAgentSession({});
setPuzzleSession(session);
enterCreateTab();
setShowCreationTypeModal(false);
setSelectionStage('puzzle-agent-workspace');
} catch (error) {
setPuzzleError(
resolvePuzzleErrorMessage(error, '开启拼图共创工作台失败。'),
);
} finally {
setIsPuzzleBusy(false);
}
}, [
enterCreateTab,
isPuzzleBusy,
resolvePuzzleErrorMessage,
setSelectionStage,
]);
await puzzleFlow.openWorkspace();
}, [puzzleFlow]);
const handleCreationHubCreateType = useCallback(
(type: PlatformCreationTypeId) => {
@@ -789,206 +905,34 @@ export function PlatformEntryFlowShellImpl({
);
const leaveBigFishFlow = useCallback(() => {
setBigFishError(null);
setBigFishRun(null);
setStreamingBigFishReplyText('');
setIsStreamingBigFishReply(false);
enterCreateTab();
setSelectionStage('platform');
}, [enterCreateTab, setSelectionStage]);
bigFishFlow.leaveFlow();
}, [bigFishFlow]);
const leavePuzzleFlow = useCallback(() => {
setPuzzleError(null);
setPuzzleOperation(null);
setPuzzleRun(null);
setStreamingPuzzleReplyText('');
setIsStreamingPuzzleReply(false);
enterCreateTab();
setSelectionStage('platform');
}, [enterCreateTab, setSelectionStage]);
puzzleFlow.leaveFlow();
}, [puzzleFlow]);
const submitBigFishMessage = useCallback(
async (payload: SendBigFishMessageRequest) => {
if (!bigFishSession || isStreamingBigFishReply) {
return;
}
const submitBigFishMessage = bigFishFlow.submitMessage;
const optimisticMessage = {
id: payload.clientMessageId,
role: 'user',
kind: 'chat',
text: payload.text.trim(),
createdAt: new Date().toISOString(),
};
const submitPuzzleMessage = puzzleFlow.submitMessage;
setBigFishError(null);
setStreamingBigFishReplyText('');
setIsStreamingBigFishReply(true);
setBigFishSession((current) =>
current
? {
...current,
messages: [...current.messages, optimisticMessage],
updatedAt: optimisticMessage.createdAt,
}
: current,
const executeBigFishAction = bigFishFlow.executeAction;
const executePuzzleAction = puzzleFlow.executeAction;
useEffect(() => {
if (selectionStage === 'big-fish-result' && !bigFishSession?.draft) {
setSelectionStage(
bigFishSession ? 'big-fish-agent-workspace' : 'platform',
);
try {
const nextSession = await streamBigFishCreationMessage(
bigFishSession.sessionId,
payload,
{
onUpdate: setStreamingBigFishReplyText,
},
);
setBigFishSession(nextSession);
setStreamingBigFishReplyText('');
} catch (error) {
setBigFishError(
resolveBigFishErrorMessage(error, '发送大鱼吃小鱼共创消息失败。'),
);
} finally {
setIsStreamingBigFishReply(false);
}
},
[bigFishSession, isStreamingBigFishReply, resolveBigFishErrorMessage],
);
const submitPuzzleMessage = useCallback(
async (payload: SendPuzzleAgentMessageRequest) => {
if (!puzzleSession || isStreamingPuzzleReply) {
return;
}
const optimisticMessage = {
id: payload.clientMessageId,
role: 'user',
kind: 'chat',
text: payload.text.trim(),
createdAt: new Date().toISOString(),
} satisfies PuzzleAgentSessionSnapshot['messages'][number];
setPuzzleError(null);
setStreamingPuzzleReplyText('');
setIsStreamingPuzzleReply(true);
setPuzzleSession((current) =>
current
? {
...current,
messages: [...current.messages, optimisticMessage],
updatedAt: optimisticMessage.createdAt,
}
: current,
);
try {
const nextSession = await streamPuzzleAgentMessage(
puzzleSession.sessionId,
payload,
{
onUpdate: setStreamingPuzzleReplyText,
},
);
setPuzzleSession(nextSession);
setStreamingPuzzleReplyText('');
} catch (error) {
setPuzzleError(
resolvePuzzleErrorMessage(error, '发送拼图共创消息失败。'),
);
} finally {
setIsStreamingPuzzleReply(false);
}
},
[isStreamingPuzzleReply, puzzleSession, resolvePuzzleErrorMessage],
);
const executeBigFishAction = useCallback(
async (payload: ExecuteBigFishActionRequest) => {
if (!bigFishSession || isBigFishBusy) {
return;
}
setIsBigFishBusy(true);
setBigFishError(null);
try {
const { session } = await executeBigFishCreationAction(
bigFishSession.sessionId,
payload,
);
setBigFishSession(session);
if (payload.action === 'big_fish_compile_draft') {
setSelectionStage('big-fish-result');
}
} catch (error) {
setBigFishError(
resolveBigFishErrorMessage(error, '执行大鱼吃小鱼操作失败。'),
);
} finally {
setIsBigFishBusy(false);
}
},
[
bigFishSession,
isBigFishBusy,
resolveBigFishErrorMessage,
setSelectionStage,
],
);
const executePuzzleAction = useCallback(
async (payload: PuzzleAgentActionRequest) => {
if (!puzzleSession || isPuzzleBusy) {
return;
}
setIsPuzzleBusy(true);
setPuzzleError(null);
try {
const { operation } = await executePuzzleAgentAction(
puzzleSession.sessionId,
payload,
);
setPuzzleOperation(operation);
if (payload.action === 'publish_puzzle_work') {
await refreshPuzzleShelf();
}
const { session } = await getPuzzleAgentSession(
puzzleSession.sessionId,
);
setPuzzleSession(session);
if (payload.action === 'compile_puzzle_draft') {
setSelectionStage('puzzle-result');
}
if (
payload.action === 'publish_puzzle_work' &&
session.publishedProfileId
) {
const galleryDetail = await getPuzzleGalleryDetail(
session.publishedProfileId,
);
setSelectedPuzzleDetail(galleryDetail.item);
setSelectionStage('puzzle-gallery-detail');
}
} catch (error) {
setPuzzleError(resolvePuzzleErrorMessage(error, '执行拼图操作失败。'));
} finally {
setIsPuzzleBusy(false);
}
},
[
isPuzzleBusy,
puzzleSession,
refreshPuzzleShelf,
resolvePuzzleErrorMessage,
setSelectionStage,
],
);
}
if (selectionStage === 'big-fish-runtime' && !bigFishRun) {
setSelectionStage(bigFishSession?.draft ? 'big-fish-result' : 'platform');
}
}, [bigFishRun, bigFishSession, selectionStage, setSelectionStage]);
const startBigFishRun = useCallback(async () => {
if (!bigFishSession || isBigFishBusy) {
@@ -1327,6 +1271,7 @@ export function PlatformEntryFlowShellImpl({
void deletePuzzleWork(work.profileId)
.then((response) => {
setPuzzleWorks(response.items);
void refreshPuzzleGallery();
})
.catch((error) => {
setPuzzleError(resolvePuzzleErrorMessage(error, '删除拼图作品失败。'));
@@ -1336,7 +1281,12 @@ export function PlatformEntryFlowShellImpl({
});
});
},
[deletingCreationWorkId, resolvePuzzleErrorMessage, runProtectedAction],
[
deletingCreationWorkId,
refreshPuzzleGallery,
resolvePuzzleErrorMessage,
runProtectedAction,
],
);
const openPuzzleDetail = useCallback(
@@ -1360,80 +1310,28 @@ export function PlatformEntryFlowShellImpl({
const openPuzzleDraft = useCallback(
async (item: PuzzleWorkSummary) => {
const sessionId = item.sourceSessionId?.trim();
if (!sessionId) {
setPuzzleError('这份拼图草稿缺少会话信息,请重新开始创作。');
return;
}
setIsPuzzleBusy(true);
setPuzzleError(null);
setPuzzleOperation(null);
setPuzzleRun(null);
setSelectedPuzzleDetail(null);
setStreamingPuzzleReplyText('');
setIsStreamingPuzzleReply(false);
try {
const { session } = await getPuzzleAgentSession(sessionId);
setPuzzleSession(session);
enterCreateTab();
setSelectionStage(session.draft ? 'puzzle-result' : 'puzzle-agent-workspace');
} catch (error) {
const restoredSession = await puzzleFlow.restoreDraft(item.sourceSessionId);
if (!restoredSession) {
await refreshPuzzleShelf().catch(() => undefined);
setPuzzleError(resolvePuzzleErrorMessage(error, '读取拼图创作草稿失败。'));
enterCreateTab();
setSelectionStage('platform');
} finally {
setIsPuzzleBusy(false);
}
},
[
enterCreateTab,
refreshPuzzleShelf,
resolvePuzzleErrorMessage,
setSelectionStage,
],
[puzzleFlow, refreshPuzzleShelf],
);
const openBigFishDraft = useCallback(
async (item: BigFishWorkSummary) => {
const sessionId = item.sourceSessionId?.trim();
if (!sessionId) {
setBigFishError('这份大鱼吃小鱼草稿缺少会话信息,请重新开始创作。');
return;
}
setIsBigFishBusy(true);
setBigFishError(null);
setBigFishRun(null);
setStreamingBigFishReplyText('');
setIsStreamingBigFishReply(false);
try {
const { session } = await getBigFishCreationSession(sessionId);
setBigFishSession(session);
enterCreateTab();
setSelectionStage(
session.draft ? 'big-fish-result' : 'big-fish-agent-workspace',
);
} catch (error) {
const restoredSession = await bigFishFlow.restoreDraft(
item.sourceSessionId,
);
if (!restoredSession) {
await refreshBigFishShelf().catch(() => undefined);
setBigFishError(
resolveBigFishErrorMessage(error, '读取大鱼吃小鱼创作草稿失败。'),
);
enterCreateTab();
setSelectionStage('platform');
} finally {
setIsBigFishBusy(false);
}
},
[
enterCreateTab,
refreshBigFishShelf,
resolveBigFishErrorMessage,
setSelectionStage,
],
[bigFishFlow, refreshBigFishShelf],
);
const startBigFishRunFromWork = useCallback(
@@ -1450,7 +1348,7 @@ export function PlatformEntryFlowShellImpl({
try {
const { session } = await getBigFishCreationSession(sessionId);
const { run } = await startBigFishRuntimeRun(sessionId);
setBigFishSession(session);
bigFishFlow.setSession(session);
setBigFishRun(run);
setSelectionStage('big-fish-runtime');
} catch (error) {
@@ -1461,9 +1359,15 @@ export function PlatformEntryFlowShellImpl({
setIsBigFishBusy(false);
}
},
[resolveBigFishErrorMessage, setSelectionStage],
[bigFishFlow, resolveBigFishErrorMessage, setSelectionStage],
);
useEffect(() => {
if (selectionStage === 'platform') {
void refreshPuzzleGallery();
}
}, [refreshPuzzleGallery, selectionStage]);
useEffect(() => {
if (
(platformBootstrap.platformTab === 'create' ||
@@ -1610,7 +1514,7 @@ export function PlatformEntryFlowShellImpl({
saveEntries={platformBootstrap.saveEntries}
saveError={platformBootstrap.saveError}
featuredEntries={featuredGalleryEntries}
latestEntries={platformBootstrap.publishedGalleryEntries}
latestEntries={latestGalleryEntries}
myEntries={platformBootstrap.savedCustomWorldEntries}
historyEntries={platformBootstrap.historyEntries}
profileDashboard={platformBootstrap.profileDashboard}
@@ -1636,6 +1540,11 @@ export function PlatformEntryFlowShellImpl({
onOpenCreateWorld={openCreationTypePicker}
onOpenCreateTypePicker={openCreationTypePicker}
onOpenGalleryDetail={(entry) => {
if (isPuzzleGalleryEntry(entry)) {
void openPuzzleDetail(entry.profileId);
return;
}
runProtectedAction(() => {
void detailNavigation.openGalleryDetail(entry);
});
@@ -1658,6 +1567,7 @@ export function PlatformEntryFlowShellImpl({
void platformBootstrap.refreshProfileDashboard();
}
}}
onRechargeSuccess={platformBootstrap.refreshProfileDashboard}
/>
</motion.div>
)}
@@ -1788,6 +1698,7 @@ export function PlatformEntryFlowShellImpl({
<BigFishAgentWorkspace
session={bigFishSession}
streamingReplyText={streamingBigFishReplyText}
isStreamingReply={isStreamingBigFishReply}
isBusy={isBigFishBusy || isStreamingBigFishReply}
error={bigFishError}
onBack={leaveBigFishFlow}
@@ -1871,6 +1782,7 @@ export function PlatformEntryFlowShellImpl({
session={puzzleSession}
activeOperation={puzzleOperation}
streamingReplyText={streamingPuzzleReplyText}
isStreamingReply={isStreamingPuzzleReply}
isBusy={isPuzzleBusy || isStreamingPuzzleReply}
error={puzzleError}
onBack={leavePuzzleFlow}
@@ -1985,7 +1897,7 @@ export function PlatformEntryFlowShellImpl({
onInterrupt={undefined}
backLabel="返回工作区"
settingActionLabel={null}
retryLabel="重新生成草稿"
retryLabel="继续生成草稿"
settingTitle="当前世界信息"
settingDescription={null}
progressTitle="世界草稿生成进度"
@@ -2024,26 +1936,7 @@ export function PlatformEntryFlowShellImpl({
onBack={
sessionController.isAgentDraftResultView
? () => {
void (async () => {
const currentProfile =
sessionController.generatedCustomWorldProfile;
if (!currentProfile) {
leaveAgentDraftResult();
return;
}
await autosaveCoordinator.syncAgentDraftResultProfile(
currentProfile,
);
leaveAgentDraftResult();
})().catch((error) => {
sessionController.setCustomWorldError(
resolveRpgCreationErrorMessage(
error,
'返回创作前同步草稿失败。',
),
);
});
leaveAgentDraftResult();
}
: leaveCustomWorldResult
}

View File

@@ -0,0 +1,294 @@
import { useCallback, useState } from 'react';
import type { TextStreamOptions } from '../../services/aiTypes';
import type { SelectionStage } from './platformEntryTypes';
type CreationAgentMessageLike = {
clientMessageId: string;
text: string;
};
type CreationAgentSessionLike = {
sessionId: string;
draft?: unknown;
messages: Array<{
id: string;
role: string;
kind?: string;
text: string;
createdAt?: string;
}>;
updatedAt?: string;
};
type CreationAgentClientAdapter<
TSession extends CreationAgentSessionLike,
TCreatePayload,
TCreateResponse,
TMessagePayload extends CreationAgentMessageLike,
TActionPayload,
TActionResponse,
> = {
createSession: (payload: TCreatePayload) => Promise<TCreateResponse>;
getSession: (sessionId: string) => Promise<TCreateResponse>;
streamMessage: (
sessionId: string,
payload: TMessagePayload,
options?: TextStreamOptions,
) => Promise<TSession>;
executeAction: (
sessionId: string,
payload: TActionPayload,
) => Promise<TActionResponse>;
selectSession: (response: TCreateResponse) => TSession;
};
type PlatformCreationAgentFlowControllerOptions<
TSession extends CreationAgentSessionLike,
TCreatePayload,
TCreateResponse,
TMessagePayload extends CreationAgentMessageLike,
TActionPayload,
TActionResponse,
> = {
client: CreationAgentClientAdapter<
TSession,
TCreatePayload,
TCreateResponse,
TMessagePayload,
TActionPayload,
TActionResponse
>;
createPayload: TCreatePayload;
workspaceStage: SelectionStage;
resultStage: SelectionStage;
platformStage: SelectionStage;
isCompileAction: (payload: TActionPayload) => boolean;
resolveErrorMessage: (error: unknown, fallback: string) => string;
errorMessages: {
open: string;
restoreMissingSession: string;
restore: string;
submit: string;
execute: string;
};
enterCreateTab: () => void;
setSelectionStage: (stage: SelectionStage) => void;
onSessionOpened?: () => void;
onActionComplete?: (params: {
payload: TActionPayload;
response: TActionResponse;
session: TSession;
setSession: (session: TSession) => void;
}) => Promise<void> | void;
};
function buildOptimisticMessage<TMessagePayload extends CreationAgentMessageLike>(
payload: TMessagePayload,
) {
return {
id: payload.clientMessageId,
role: 'user',
kind: 'chat',
text: payload.text.trim(),
createdAt: new Date().toISOString(),
};
}
/**
* 轻量作品 Agent 创作流程的通用前端控制器。
* 这里只处理跨玩法一致的会话、流式消息、忙碌态与草稿恢复,玩法结果页和运行态动作留给外层。
*/
export function usePlatformCreationAgentFlowController<
TSession extends CreationAgentSessionLike,
TCreatePayload,
TCreateResponse,
TMessagePayload extends CreationAgentMessageLike,
TActionPayload,
TActionResponse,
>(
options: PlatformCreationAgentFlowControllerOptions<
TSession,
TCreatePayload,
TCreateResponse,
TMessagePayload,
TActionPayload,
TActionResponse
>,
) {
const [session, setSession] = useState<TSession | null>(null);
const [error, setError] = useState<string | null>(null);
const [isBusy, setIsBusy] = useState(false);
const [streamingReplyText, setStreamingReplyText] = useState('');
const [isStreamingReply, setIsStreamingReply] = useState(false);
const openWorkspace = useCallback(async () => {
if (isBusy) {
return;
}
setIsBusy(true);
setError(null);
setStreamingReplyText('');
setIsStreamingReply(false);
try {
const response = await options.client.createSession(options.createPayload);
setSession(options.client.selectSession(response));
options.enterCreateTab();
options.onSessionOpened?.();
options.setSelectionStage(options.workspaceStage);
} catch (caughtError) {
setError(
options.resolveErrorMessage(caughtError, options.errorMessages.open),
);
} finally {
setIsBusy(false);
}
}, [isBusy, options]);
const restoreDraft = useCallback(
async (sessionId: string | null | undefined) => {
const normalizedSessionId = sessionId?.trim();
if (!normalizedSessionId) {
setError(options.errorMessages.restoreMissingSession);
return null;
}
setIsBusy(true);
setError(null);
setStreamingReplyText('');
setIsStreamingReply(false);
try {
const response = await options.client.getSession(normalizedSessionId);
const nextSession = options.client.selectSession(response);
setSession(nextSession);
options.enterCreateTab();
options.setSelectionStage(
nextSession.draft ? options.resultStage : options.workspaceStage,
);
return nextSession;
} catch (caughtError) {
setError(
options.resolveErrorMessage(caughtError, options.errorMessages.restore),
);
options.enterCreateTab();
options.setSelectionStage(options.platformStage);
return null;
} finally {
setIsBusy(false);
}
},
[options],
);
const submitMessage = useCallback(
async (payload: TMessagePayload) => {
if (!session || isStreamingReply) {
return;
}
const optimisticMessage = buildOptimisticMessage(payload);
setError(null);
setStreamingReplyText('');
setIsStreamingReply(true);
setSession((current) =>
current
? {
...current,
messages: [...current.messages, optimisticMessage],
updatedAt: optimisticMessage.createdAt,
}
: current,
);
try {
const nextSession = await options.client.streamMessage(
session.sessionId,
payload,
{
onUpdate: setStreamingReplyText,
},
);
setSession(nextSession);
setStreamingReplyText('');
} catch (caughtError) {
setError(
options.resolveErrorMessage(caughtError, options.errorMessages.submit),
);
} finally {
setIsStreamingReply(false);
}
},
[isStreamingReply, options, session],
);
const executeAction = useCallback(
async (payload: TActionPayload) => {
if (!session || isBusy) {
return;
}
setIsBusy(true);
setError(null);
try {
const response = await options.client.executeAction(
session.sessionId,
payload,
);
await options.onActionComplete?.({
payload,
response,
session,
setSession,
});
if (options.isCompileAction(payload)) {
options.setSelectionStage(options.resultStage);
}
} catch (caughtError) {
setError(
options.resolveErrorMessage(caughtError, options.errorMessages.execute),
);
} finally {
setIsBusy(false);
}
},
[isBusy, options, session],
);
const leaveFlow = useCallback(() => {
setError(null);
setStreamingReplyText('');
setIsStreamingReply(false);
options.enterCreateTab();
options.setSelectionStage(options.platformStage);
}, [options]);
const resetTransientState = useCallback(() => {
setError(null);
setStreamingReplyText('');
setIsStreamingReply(false);
}, []);
return {
session,
setSession,
error,
setError,
isBusy,
setIsBusy,
streamingReplyText,
setStreamingReplyText,
isStreamingReply,
setIsStreamingReply,
openWorkspace,
restoreDraft,
submitMessage,
executeAction,
leaveFlow,
resetTransientState,
};
}

View File

@@ -23,6 +23,7 @@ type PuzzleAgentWorkspaceProps = {
session: PuzzleAgentSessionSnapshot | null;
activeOperation?: PuzzleAgentOperationRecord | null;
streamingReplyText?: string;
isStreamingReply?: boolean;
isBusy?: boolean;
error?: string | null;
onBack: () => void;
@@ -87,6 +88,7 @@ export function PuzzleAgentWorkspace({
session,
activeOperation = null,
streamingReplyText = '',
isStreamingReply = false,
isBusy = false,
error = null,
onBack,
@@ -102,7 +104,7 @@ export function PuzzleAgentWorkspace({
primaryActionLabel="生成结果页"
activeOperation={mapPuzzleOperation(activeOperation)}
streamingReplyText={streamingReplyText}
isStreamingReply={Boolean(streamingReplyText)}
isStreamingReply={isStreamingReply}
isBusy={isBusy}
error={error}
quickActions={createCreationAgentChatQuickActions()}

View File

@@ -0,0 +1 @@
export { WorldFoundationEditor as default } from './RpgCreationEntityEditorShared';

View File

@@ -2,6 +2,7 @@ import type { CustomWorldProfile } from '../../types';
import CampSceneEditor from './CustomWorldCampEditorSection';
import WorldCoverEditor from './CustomWorldCoverEditorSection';
import WorldFoundationEditor from './CustomWorldFoundationEditorSection';
import LandmarkEditor from './CustomWorldLandmarkEditorSection';
import PlayableNpcEditor, {
StoryNpcEditor,
@@ -15,6 +16,7 @@ import {
export type RpgCreationEditorTarget =
| { kind: 'world' }
| { kind: 'foundation' }
| { kind: 'cover' }
| { kind: 'camp' }
| { kind: 'playable'; mode: 'create' }
@@ -55,6 +57,16 @@ export function RpgCreationEntityEditorModal({
);
}
if (target.kind === 'foundation') {
return (
<WorldFoundationEditor
profile={profile}
onSave={onProfileChange}
onClose={onClose}
/>
);
}
if (target.kind === 'cover') {
return (
<WorldCoverEditor

View File

@@ -32,6 +32,14 @@ import {
buildDefaultCustomWorldCoverProfile,
resolveCustomWorldCoverPresentation,
} from '../../services/customWorldCover';
import {
getCustomWorldFoundationAnchorContent,
parseFoundationTagText,
type CustomWorldFoundationEntryId,
} from '../../services/customWorldFoundationEntries';
import {
createEmptyCustomWorldCreatorIntent,
} from '../../services/customWorldCreatorIntent';
import {
type CustomWorldCoverAssetResult,
generateCustomWorldCoverImage,
@@ -94,6 +102,7 @@ import {
export type RpgCreationEditorTarget =
| { kind: 'world' }
| { kind: 'foundation' }
| { kind: 'cover' }
| { kind: 'camp' }
| { kind: 'playable'; mode: 'create' }
@@ -4522,6 +4531,270 @@ export function WorldEditor({
);
}
type FoundationDraft = Record<CustomWorldFoundationEntryId, string>;
const FOUNDATION_EDITOR_FIELDS: Array<{
id: CustomWorldFoundationEntryId;
label: string;
rows: number;
}> = [
{ id: 'world-promise', label: '世界承诺', rows: 4 },
{ id: 'player-fantasy', label: '玩家幻想', rows: 4 },
{ id: 'theme-boundary', label: '主题边界', rows: 4 },
{ id: 'player-entry-point', label: '玩家切入口', rows: 4 },
{ id: 'core-conflict', label: '核心冲突', rows: 4 },
{ id: 'key-relationships', label: '关键关系', rows: 4 },
{ id: 'hidden-lines', label: '暗线与揭示', rows: 4 },
{ id: 'iconic-elements', label: '标志元素', rows: 4 },
];
function buildFoundationDraft(profile: CustomWorldProfile): FoundationDraft {
const anchorContent = getCustomWorldFoundationAnchorContent(profile);
return {
'world-promise': [
anchorContent.worldPromise?.hook || '',
anchorContent.worldPromise?.differentiator || '',
anchorContent.worldPromise?.desiredExperience || '',
]
.filter(Boolean)
.join(''),
'player-fantasy': [
anchorContent.playerFantasy?.playerRole || '',
anchorContent.playerFantasy?.corePursuit || '',
anchorContent.playerFantasy?.fearOfLoss || '',
]
.filter(Boolean)
.join(''),
'theme-boundary': [
anchorContent.themeBoundary?.toneKeywords.join('、') || '',
anchorContent.themeBoundary?.aestheticDirectives.join('、') || '',
anchorContent.themeBoundary?.forbiddenDirectives.length
? `避免:${anchorContent.themeBoundary.forbiddenDirectives.join('、')}`
: '',
]
.filter(Boolean)
.join(''),
'player-entry-point': [
anchorContent.playerEntryPoint?.openingIdentity || '',
anchorContent.playerEntryPoint?.openingProblem || '',
anchorContent.playerEntryPoint?.entryMotivation || '',
]
.filter(Boolean)
.join(''),
'core-conflict': [
anchorContent.coreConflict?.surfaceConflicts.join('、') || '',
anchorContent.coreConflict?.hiddenCrisis || '',
anchorContent.coreConflict?.firstTouchedConflict || '',
]
.filter(Boolean)
.join(''),
'key-relationships': anchorContent.keyRelationships
.map((entry) =>
[entry.pairs, entry.relationshipType, entry.secretOrCost]
.filter(Boolean)
.join(''),
)
.join('\n'),
'hidden-lines': [
anchorContent.hiddenLines?.hiddenTruths.join('、') || '',
anchorContent.hiddenLines?.misdirectionHints.join('、') || '',
anchorContent.hiddenLines?.revealPacing || '',
]
.filter(Boolean)
.join(''),
'iconic-elements': [
anchorContent.iconicElements?.iconicMotifs.join('、') || '',
anchorContent.iconicElements?.institutionsOrArtifacts.join('、') || '',
anchorContent.iconicElements?.hardRules.join('、') || '',
]
.filter(Boolean)
.join(''),
};
}
function splitCommaTags(value: string) {
return value
.split(/[,]/u)
.map((item) => item.trim())
.filter(Boolean);
}
function stripAvoidPrefix(value: string) {
return value.replace(/^[:]\s*/u, '').trim();
}
function applyFoundationDraftToProfile(
profile: CustomWorldProfile,
draft: FoundationDraft,
): CustomWorldProfile {
const worldPromiseTags = parseFoundationTagText(draft['world-promise']);
const playerFantasyTags = parseFoundationTagText(draft['player-fantasy']);
const themeBoundaryTags = parseFoundationTagText(draft['theme-boundary']);
const playerEntryTags = parseFoundationTagText(draft['player-entry-point']);
const coreConflictTags = parseFoundationTagText(draft['core-conflict']);
const hiddenLineTags = parseFoundationTagText(draft['hidden-lines']);
const iconicElementTags = parseFoundationTagText(draft['iconic-elements']);
const relationshipLines = draft['key-relationships']
.split(/\r?\n/u)
.map((line) => line.trim())
.filter(Boolean);
const creatorIntent =
profile.creatorIntent ?? createEmptyCustomWorldCreatorIntent('freeform');
return {
...profile,
summary: worldPromiseTags[0] || profile.summary,
subtitle: worldPromiseTags[1] || profile.subtitle,
tone: themeBoundaryTags[0] || profile.tone,
playerGoal: playerFantasyTags[1] || profile.playerGoal,
worldHook: worldPromiseTags[0] || profile.worldHook || null,
playerPremise: playerFantasyTags[0] || profile.playerPremise || null,
coreConflicts: coreConflictTags[0]
? splitCommaTags(coreConflictTags[0])
: profile.coreConflicts,
creatorIntent: {
...creatorIntent,
worldHook: worldPromiseTags[0] || creatorIntent.worldHook,
playerPremise: playerFantasyTags[0] || creatorIntent.playerPremise,
openingSituation: playerEntryTags[1] || creatorIntent.openingSituation,
themeKeywords: themeBoundaryTags[0]
? splitCommaTags(themeBoundaryTags[0])
: creatorIntent.themeKeywords,
toneDirectives: themeBoundaryTags[1]
? splitCommaTags(themeBoundaryTags[1])
: creatorIntent.toneDirectives,
coreConflicts: coreConflictTags[0]
? splitCommaTags(coreConflictTags[0])
: creatorIntent.coreConflicts,
iconicElements: iconicElementTags[0]
? splitCommaTags(iconicElementTags[0])
: creatorIntent.iconicElements,
forbiddenDirectives: themeBoundaryTags[2]
? splitCommaTags(stripAvoidPrefix(themeBoundaryTags[2]))
: creatorIntent.forbiddenDirectives,
},
anchorContent: {
worldPromise: {
hook: worldPromiseTags[0] || '',
differentiator: worldPromiseTags[1] || '',
desiredExperience: worldPromiseTags[2] || '',
},
playerFantasy: {
playerRole: playerFantasyTags[0] || '',
corePursuit: playerFantasyTags[1] || '',
fearOfLoss: playerFantasyTags[2] || '',
},
themeBoundary: {
toneKeywords: themeBoundaryTags[0]
? splitCommaTags(themeBoundaryTags[0])
: [],
aestheticDirectives: themeBoundaryTags[1]
? splitCommaTags(themeBoundaryTags[1])
: [],
forbiddenDirectives: themeBoundaryTags[2]
? splitCommaTags(stripAvoidPrefix(themeBoundaryTags[2]))
: [],
},
playerEntryPoint: {
openingIdentity: playerEntryTags[0] || '',
openingProblem: playerEntryTags[1] || '',
entryMotivation: playerEntryTags[2] || '',
},
coreConflict: {
surfaceConflicts: coreConflictTags[0]
? splitCommaTags(coreConflictTags[0])
: [],
hiddenCrisis: coreConflictTags[1] || '',
firstTouchedConflict: coreConflictTags[2] || '',
},
keyRelationships: relationshipLines.map((line) => {
const tags = parseFoundationTagText(line);
return {
pairs: tags[0] || '',
relationshipType: tags[1] || '',
secretOrCost: tags[2] || '',
};
}),
hiddenLines: {
hiddenTruths: hiddenLineTags[0] ? splitCommaTags(hiddenLineTags[0]) : [],
misdirectionHints: hiddenLineTags[1]
? splitCommaTags(hiddenLineTags[1])
: [],
revealPacing: hiddenLineTags[2] || '',
},
iconicElements: {
iconicMotifs: iconicElementTags[0]
? splitCommaTags(iconicElementTags[0])
: [],
institutionsOrArtifacts: iconicElementTags[1]
? splitCommaTags(iconicElementTags[1])
: [],
hardRules: iconicElementTags[2] ? splitCommaTags(iconicElementTags[2]) : [],
},
},
};
}
export function WorldFoundationEditor({
profile,
onSave,
onClose,
}: {
profile: CustomWorldProfile;
onSave: (profile: CustomWorldProfile) => void;
onClose: () => void;
}) {
const initialDraft = useMemo(() => buildFoundationDraft(profile), [profile]);
const [draft, setDraft] = useDraft(initialDraft);
return (
<ModalShell
title="编辑基本设定"
onClose={onClose}
panelClassName="sm:max-w-4xl"
>
<div className="space-y-4">
{FOUNDATION_EDITOR_FIELDS.map((field) => (
<Field key={field.id} label={field.label}>
<div className="space-y-3">
<TextArea
value={draft[field.id]}
onChange={(value) =>
setDraft((current) => ({
...current,
[field.id]: value,
}))
}
rows={field.rows}
/>
{draft[field.id].trim() ? (
<div className="flex flex-wrap gap-2">
{parseFoundationTagText(draft[field.id]).map((tag, index) => (
<span
key={`${field.id}-${index}-${tag}`}
className="rounded-full border border-white/10 bg-white/[0.06] px-3 py-1 text-xs leading-5 text-zinc-100"
>
{tag}
</span>
))}
</div>
) : null}
</div>
</Field>
))}
<SaveBar
onClose={onClose}
onSave={() => {
onSave(applyFoundationDraftToProfile(profile, draft));
onClose();
}}
/>
</div>
</ModalShell>
);
}
export function CampSceneEditor({
profile,
onSaveProfile,
@@ -5921,6 +6194,16 @@ function RpgCreationEntityEditorModal({
);
}
if (target.kind === 'foundation') {
return (
<WorldFoundationEditor
profile={profile}
onSave={onProfileChange}
onClose={onClose}
/>
);
}
if (target.kind === 'cover') {
return (
<WorldCoverEditor

View File

@@ -6,6 +6,7 @@ import { useState } from 'react';
import { beforeEach, expect, test, vi } from 'vitest';
import type { CustomWorldAgentSessionSnapshot } from '../../../packages/shared/src/contracts/customWorldAgent';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
import {
createRpgCreationSession,
@@ -41,6 +42,10 @@ import {
createPuzzleAgentSession,
getPuzzleAgentSession,
} from '../../services/puzzle-agent';
import {
getPuzzleGalleryDetail,
listPuzzleGallery,
} from '../../services/puzzle-gallery';
import { listPuzzleWorks } from '../../services/puzzle-works';
import type { GameState } from '../../types';
import {
@@ -118,6 +123,11 @@ vi.mock('../../services/puzzle-works', () => ({
listPuzzleWorks: vi.fn(),
}));
vi.mock('../../services/puzzle-gallery', () => ({
getPuzzleGalleryDetail: vi.fn(),
listPuzzleGallery: vi.fn(),
}));
vi.mock('../../services/big-fish-creation', () => ({
createBigFishCreationSession: vi.fn(),
executeBigFishCreationAction: vi.fn(),
@@ -958,6 +968,9 @@ beforeEach(() => {
vi.mocked(listPuzzleWorks).mockResolvedValue({
items: [],
});
vi.mocked(listPuzzleGallery).mockResolvedValue({
items: [],
});
vi.mocked(executeRpgCreationAction).mockResolvedValue({
operation: {
operationId: 'operation-draft-foundation-1',
@@ -1137,6 +1150,53 @@ test('create tab resumes agent workspace when draft has no compiled result yet',
expect(screen.queryByText('世界档案')).toBeNull();
});
test('create tab resumes agent workspace when session has no draft profile even if summary counts look compiled', async () => {
const user = userEvent.setup();
vi.mocked(listRpgCreationWorks).mockResolvedValue([
{
workId: 'draft:custom-world-agent-session-1',
sourceType: 'agent_session',
status: 'draft',
title: '潮雾列岛',
subtitle: '待完善草稿',
summary: '作品卡摘要仍带着旧对象数量,但服务端还没有草稿 profile。',
coverImageSrc: null,
coverRenderMode: 'image',
coverCharacterImageSrcs: [],
updatedAt: '2026-04-20T10:00:00.000Z',
publishedAt: null,
stage: 'clarifying',
stageLabel: '补齐关键锚点',
playableNpcCount: 2,
landmarkCount: 1,
roleVisualReadyCount: 0,
roleAnimationReadyCount: 0,
roleAssetSummaryLabel: null,
sessionId: 'custom-world-agent-session-1',
profileId: null,
canResume: true,
canEnterWorld: false,
},
]);
vi.mocked(getRpgCreationSession).mockResolvedValue({
...mockSession,
stage: 'clarifying',
draftProfile: null,
});
render(<TestWrapper withAuth />);
await openCreationHub(user);
expect(await screen.findByRole('button', { name: //u })).toBeTruthy();
await user.click(await screen.findByRole('button', { name: //u }));
expect(
await screen.findByText('Agent工作区custom-world-agent-session-1'),
).toBeTruthy();
expect(screen.queryByText('世界档案')).toBeNull();
});
test('opening a compiled draft with a missing agent session falls back to create hub', async () => {
const user = userEvent.setup();
@@ -1241,6 +1301,48 @@ test('clicking a public work while logged out routes through requireAuth', async
expect(getRpgEntryWorldGalleryDetail).not.toHaveBeenCalled();
});
test('published puzzle works appear on home and category public shelves', async () => {
const user = userEvent.setup();
const publishedPuzzleWork = {
workId: 'puzzle-work-public-1',
profileId: 'puzzle-profile-public-1',
ownerUserId: 'user-2',
sourceSessionId: 'puzzle-session-public-1',
authorDisplayName: '拼图作者',
levelName: '星桥机关',
summary: '旋转碎片并接通星桥机关。',
themeTags: ['机关', '星桥'],
coverImageSrc: null,
coverAssetId: null,
publicationStatus: 'published',
updatedAt: '2026-04-25T09:00:00.000Z',
publishedAt: '2026-04-25T09:00:00.000Z',
playCount: 3,
publishReady: true,
} satisfies PuzzleWorkSummary;
vi.mocked(listPuzzleGallery).mockResolvedValue({
items: [publishedPuzzleWork],
});
vi.mocked(getPuzzleGalleryDetail).mockResolvedValue({
item: publishedPuzzleWork,
});
render(<TestWrapper />);
await waitFor(() => {
expect(screen.getAllByText('星桥机关').length).toBeGreaterThan(0);
});
await user.click(screen.getByRole('button', { name: '分类' }));
const categoryPanel = getPlatformTabPanel('category');
expect(within(categoryPanel).getAllByText('星桥机关').length).toBeGreaterThan(0);
expect(
within(categoryPanel).getAllByRole('button', { name: //u }).length,
).toBeGreaterThan(0);
});
test('selecting RPG creation while logged out routes through requireAuth', async () => {
const user = userEvent.setup();
const requireAuth = vi.fn();
@@ -2011,30 +2113,9 @@ test('agent result view does not keep legacy publish blockers when preview uses
expect((actionButton as HTMLButtonElement).disabled).toBe(false);
});
test('agent draft result back button syncs result profile before returning to creation hub', async () => {
test('agent draft result back button returns to creation hub without syncing result profile', async () => {
const user = userEvent.setup();
vi.mocked(executeRpgCreationAction).mockResolvedValue({
operation: {
operationId: 'operation-sync-result-profile-1',
type: 'sync_result_profile',
status: 'queued',
phaseLabel: '同步结果页快照',
phaseDetail: '正在把结果页里的最新世界结构写回当前草稿。',
progress: 24,
error: null,
},
});
vi.mocked(getRpgCreationOperation).mockResolvedValue({
operationId: 'operation-sync-result-profile-1',
type: 'sync_result_profile',
status: 'completed',
phaseLabel: '结果页快照已同步',
phaseDetail: '当前结果页编辑内容已经并回 Agent 草稿主链。',
progress: 100,
error: null,
});
const resultSession = {
...mockSession,
stage: 'object_refining' as const,
@@ -2198,34 +2279,13 @@ test('agent draft result back button syncs result profile before returning to cr
sessionId === 'custom-world-agent-session-1' &&
payload?.action === 'sync_result_profile',
),
).toBe(true);
).toBe(false);
expect(screen.queryByText('世界档案')).toBeNull();
});
test('agent draft result auto-save persists the latest profile rebuilt from synced session', async () => {
test('agent draft result auto-save persists the latest profile from session draft without result sync action', async () => {
const user = userEvent.setup();
vi.mocked(executeRpgCreationAction).mockResolvedValue({
operation: {
operationId: 'operation-sync-result-profile-2',
type: 'sync_result_profile',
status: 'queued',
phaseLabel: '同步结果页快照',
phaseDetail: '正在把结果页里的最新世界结构写回当前草稿。',
progress: 24,
error: null,
},
});
vi.mocked(getRpgCreationOperation).mockResolvedValue({
operationId: 'operation-sync-result-profile-2',
type: 'sync_result_profile',
status: 'completed',
phaseLabel: '结果页快照已同步',
phaseDetail: '当前结果页编辑内容已经并回 Agent 草稿主链。',
progress: 100,
error: null,
});
const syncedSession = {
...mockSession,
stage: 'object_refining' as const,
@@ -2353,6 +2413,15 @@ test('agent draft result auto-save persists the latest profile rebuilt from sync
expect(latestSaveRequest).toEqual({
sourceAgentSessionId: 'custom-world-agent-session-1',
});
expect(
vi
.mocked(executeRpgCreationAction)
.mock.calls.some(
([sessionId, payload]) =>
sessionId === 'custom-world-agent-session-1' &&
payload?.action === 'sync_result_profile',
),
).toBe(false);
});
test('agent draft result can open from server result preview without embedded legacyResultProfile', async () => {

View File

@@ -0,0 +1,176 @@
/* @vitest-environment jsdom */
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, expect, test, vi } from 'vitest';
import { AuthUiContext } from '../auth/AuthUiContext';
import { RpgEntryHomeView } from './RpgEntryHomeView';
vi.mock('../../services/rpg-entry/rpgProfileClient', () => ({
getRpgProfileRechargeCenter: vi.fn(async () => ({
walletBalance: 0,
membership: {
status: 'normal',
tier: 'normal',
startedAt: null,
expiresAt: null,
updatedAt: null,
},
pointProducts: [
{
productId: 'points_10',
title: '10积分',
priceCents: 100,
kind: 'points',
pointsAmount: 10,
bonusPoints: 19,
durationDays: 0,
badgeLabel: '首充送积分',
description: '首充送19积分',
tier: 'normal',
},
],
membershipProducts: [
{
productId: 'member_month',
title: '月卡',
priceCents: 2800,
kind: 'membership',
pointsAmount: 0,
bonusPoints: 0,
durationDays: 30,
badgeLabel: '',
description: '30天会员',
tier: 'month',
},
],
benefits: [
{
benefitName: '免积分回合数',
normalValue: '30',
monthValue: '100',
seasonValue: '100',
yearValue: '100',
},
],
latestOrder: null,
hasPointsRecharged: false,
})),
createRpgProfileRechargeOrder: vi.fn(async () => ({
order: {
orderId: 'order-1',
productId: 'points_10',
productTitle: '10积分',
kind: 'points',
amountCents: 100,
status: 'paid',
paymentChannel: 'mock',
paidAt: '2026-04-25T10:00:00Z',
createdAt: '2026-04-25T10:00:00Z',
pointsDelta: 29,
membershipExpiresAt: null,
},
center: {
walletBalance: 29,
membership: {
status: 'normal',
tier: 'normal',
startedAt: null,
expiresAt: null,
updatedAt: null,
},
pointProducts: [],
membershipProducts: [],
benefits: [],
latestOrder: null,
hasPointsRecharged: true,
},
})),
}));
vi.mock('../ResolvedAssetImage', () => ({
ResolvedAssetImage: () => null,
}));
function renderProfileView(onRechargeSuccess = vi.fn()) {
return render(
<AuthUiContext.Provider
value={{
user: {
id: 'user-1',
username: 'tester',
displayName: '测试玩家',
loginMethod: 'password',
bindingStatus: 'active',
phone: null,
createdAt: null,
},
canAccessProtectedData: true,
openLoginModal: vi.fn(),
requireAuth: (action) => action(),
openSettingsModal: vi.fn(),
openAccountModal: vi.fn(),
logout: vi.fn(async () => undefined),
musicVolume: 0.42,
setMusicVolume: vi.fn(),
platformTheme: 'light',
setPlatformTheme: vi.fn(),
isHydratingSettings: false,
isPersistingSettings: false,
settingsError: null,
}}
>
<RpgEntryHomeView
activeTab="profile"
onTabChange={vi.fn()}
hasSavedGame={false}
savedSnapshot={null}
saveEntries={[]}
saveError={null}
featuredEntries={[]}
latestEntries={[]}
myEntries={[]}
historyEntries={[]}
profileDashboard={{
walletBalance: 0,
totalPlayTimeMs: 0,
playedWorldCount: 0,
updatedAt: null,
}}
isLoadingPlatform={false}
isLoadingDashboard={false}
isResumingSaveWorldKey={null}
platformError={null}
dashboardError={null}
onContinueGame={vi.fn()}
onResumeSave={vi.fn()}
onOpenCreateWorld={vi.fn()}
onOpenCreateTypePicker={vi.fn()}
onOpenGalleryDetail={vi.fn()}
onOpenLibraryDetail={vi.fn()}
onSearchPublicCode={vi.fn()}
onRechargeSuccess={onRechargeSuccess}
/>
</AuthUiContext.Provider>,
);
}
afterEach(() => {
vi.clearAllMocks();
});
test('opens recharge modal and submits points product', async () => {
const user = userEvent.setup();
const onRechargeSuccess = vi.fn();
renderProfileView(onRechargeSuccess);
await user.click(screen.getByText('会员充值'));
expect(await screen.findByText('账户充值')).toBeTruthy();
expect(await screen.findByText('10积分')).toBeTruthy();
await user.click(screen.getByText('首充送19积分'));
await waitFor(() => expect(onRechargeSuccess).toHaveBeenCalledTimes(1));
});

View File

@@ -29,15 +29,20 @@ import {
} from 'react';
import type {
CustomWorldGalleryCard,
CustomWorldLibraryEntry,
PlatformBrowseHistoryEntry,
ProfileDashboardCardKey,
ProfileDashboardSummary,
ProfileRechargeCenterResponse,
ProfileRechargeProduct,
ProfileSaveArchiveSummary,
} from '../../../packages/shared/src/contracts/runtime';
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
import type { AuthUser } from '../../services/authService';
import {
createRpgProfileRechargeOrder,
getRpgProfileRechargeCenter,
} from '../../services/rpg-entry/rpgProfileClient';
import type { CustomWorldProfile } from '../../types';
import { useAuthUi } from '../auth/AuthUiContext';
import { ResolvedAssetImage } from '../ResolvedAssetImage';
@@ -46,12 +51,19 @@ import {
buildPlatformWorldTags,
describePlatformThemeLabel,
formatPlatformWorldTime,
isPuzzleGalleryEntry,
type PlatformPublicGalleryCard,
type PlatformWorldCardLike,
resolvePlatformWorldCoverImage,
resolvePlatformWorldLeadPortrait,
} from './rpgEntryWorldPresentation';
export type PlatformHomeTab = 'home' | 'category' | 'create' | 'saves' | 'profile';
export type PlatformHomeTab =
| 'home'
| 'category'
| 'create'
| 'saves'
| 'profile';
export interface RpgEntryHomeViewProps {
activeTab: PlatformHomeTab;
onTabChange: (tab: PlatformHomeTab) => void;
@@ -59,8 +71,8 @@ export interface RpgEntryHomeViewProps {
savedSnapshot: HydratedSavedGameSnapshot | null;
saveEntries: ProfileSaveArchiveSummary[];
saveError: string | null;
featuredEntries: CustomWorldGalleryCard[];
latestEntries: CustomWorldGalleryCard[];
featuredEntries: PlatformPublicGalleryCard[];
latestEntries: PlatformPublicGalleryCard[];
myEntries: CustomWorldLibraryEntry<CustomWorldProfile>[];
historyEntries: PlatformBrowseHistoryEntry[];
profileDashboard: ProfileDashboardSummary | null;
@@ -73,7 +85,7 @@ export interface RpgEntryHomeViewProps {
onResumeSave: (entry: ProfileSaveArchiveSummary) => void;
onOpenCreateWorld: () => void;
onOpenCreateTypePicker: () => void;
onOpenGalleryDetail: (entry: CustomWorldGalleryCard) => void;
onOpenGalleryDetail: (entry: PlatformPublicGalleryCard) => void;
onOpenLibraryDetail: (
entry: CustomWorldLibraryEntry<CustomWorldProfile>,
) => void;
@@ -84,6 +96,7 @@ export interface RpgEntryHomeViewProps {
onSearchPublicCode?: (keyword: string) => void | Promise<void>;
isSearchingPublicCode?: boolean;
onOpenProfileDashboardCard?: (cardKey: ProfileDashboardCardKey) => void;
onRechargeSuccess?: () => void | Promise<void>;
createTabContent?: ReactNode;
}
@@ -296,7 +309,7 @@ function WorldCard({
))
) : (
<span className="platform-pill platform-pill--neutral px-2.5">
{describePlatformThemeLabel(entry.themeMode)}
{describePublicGalleryCardKind(entry)}
</span>
)}
</div>
@@ -553,7 +566,7 @@ function DesktopTrendingItem({
rank,
onClick,
}: {
entry: CustomWorldGalleryCard;
entry: PlatformPublicGalleryCard;
rank: number;
onClick: () => void;
}) {
@@ -600,7 +613,9 @@ function DesktopTrendingItem({
))
) : (
<span className="platform-pill platform-pill--neutral px-2.5">
{describePlatformThemeLabel(entry.themeMode)}
{isPuzzleGalleryEntry(entry)
? '拼图'
: describePlatformThemeLabel(entry.themeMode)}
</span>
)}
</div>
@@ -612,16 +627,16 @@ function DesktopTrendingItem({
}
function buildPublicCategoryGroups(
featuredEntries: CustomWorldGalleryCard[],
latestEntries: CustomWorldGalleryCard[],
featuredEntries: PlatformPublicGalleryCard[],
latestEntries: PlatformPublicGalleryCard[],
) {
const publicEntryMap = new Map<string, CustomWorldGalleryCard>();
const publicEntryMap = new Map<string, PlatformPublicGalleryCard>();
[...featuredEntries, ...latestEntries].forEach((entry) => {
publicEntryMap.set(`${entry.ownerUserId}:${entry.profileId}`, entry);
publicEntryMap.set(buildPublicGalleryCardKey(entry), entry);
});
const categoryMap = new Map<string, CustomWorldGalleryCard[]>();
const categoryMap = new Map<string, PlatformPublicGalleryCard[]>();
Array.from(publicEntryMap.values()).forEach((entry) => {
const tags = buildPlatformWorldTags(entry)
.map((tag) => tag.trim())
@@ -646,6 +661,16 @@ function buildPublicCategoryGroups(
});
}
function buildPublicGalleryCardKey(entry: PlatformPublicGalleryCard) {
return `${isPuzzleGalleryEntry(entry) ? 'puzzle' : 'rpg'}:${entry.ownerUserId}:${entry.profileId}`;
}
function describePublicGalleryCardKind(entry: PlatformPublicGalleryCard) {
return isPuzzleGalleryEntry(entry)
? '拼图'
: describePlatformThemeLabel(entry.themeMode);
}
function formatSnapshotTime(value: string | null | undefined) {
if (!value) {
return '刚刚保存';
@@ -823,11 +848,181 @@ function ProfileShortcutButton({
);
}
function formatRechargePrice(priceCents: number) {
const yuan = priceCents / 100;
return Number.isInteger(yuan) ? `¥${yuan}` : `¥${yuan.toFixed(2)}`;
}
function AccountRechargeModal({
center,
activeTab,
isLoading,
isSubmitting,
error,
onTabChange,
onClose,
onSelectProduct,
}: {
center: ProfileRechargeCenterResponse | null;
activeTab: 'points' | 'membership';
isLoading: boolean;
isSubmitting: string | null;
error: string | null;
onTabChange: (tab: 'points' | 'membership') => void;
onClose: () => void;
onSelectProduct: (product: ProfileRechargeProduct) => void;
}) {
const visibleProducts =
activeTab === 'points'
? (center?.pointProducts ?? [])
: (center?.membershipProducts ?? []);
return (
<div className="fixed inset-0 z-[80] flex items-center justify-center bg-black/42 px-3 py-5">
<div className="relative max-h-[min(92vh,46rem)] w-full max-w-[28rem] overflow-hidden rounded-[1.35rem] bg-white text-zinc-950 shadow-2xl">
<button
type="button"
onClick={onClose}
className="absolute right-3 top-2 z-10 flex h-8 w-8 items-center justify-center rounded-full text-[#ff4056]"
aria-label="关闭账户充值"
>
×
</button>
<div className="max-h-[min(92vh,46rem)] overflow-y-auto px-5 pb-5 pt-4">
<div className="text-center text-2xl font-black"></div>
<div className="mt-4 grid grid-cols-2 rounded-xl bg-zinc-100 p-1">
<button
type="button"
onClick={() => onTabChange('points')}
className={`rounded-lg px-4 py-3 text-sm font-black transition ${
activeTab === 'points'
? 'bg-white text-[#ff4056] shadow'
: 'text-zinc-500'
}`}
>
</button>
<button
type="button"
onClick={() => onTabChange('membership')}
className={`rounded-lg px-4 py-3 text-sm font-black transition ${
activeTab === 'membership'
? 'bg-white text-[#ff4056] shadow'
: 'text-zinc-500'
}`}
>
</button>
</div>
{error ? (
<div className="mt-4 rounded-xl border border-rose-200 bg-rose-50 px-3 py-2 text-sm text-rose-700">
{error}
</div>
) : null}
{isLoading ? (
<div className="mt-5 grid grid-cols-2 gap-3 sm:grid-cols-3">
{Array.from({ length: activeTab === 'points' ? 6 : 3 }).map(
(_, index) => (
<div
key={index}
className="h-24 animate-pulse rounded-xl bg-zinc-100"
/>
),
)}
</div>
) : activeTab === 'points' ? (
<div className="mt-5 grid grid-cols-2 gap-3 sm:grid-cols-3">
{visibleProducts.map((product) => (
<button
type="button"
key={product.productId}
disabled={Boolean(isSubmitting)}
onClick={() => onSelectProduct(product)}
className="relative min-h-[8.45rem] overflow-hidden rounded-xl border border-zinc-200 bg-white text-center shadow-sm transition hover:border-[#ff4056] disabled:opacity-70"
>
<div
className={`h-8 px-2 py-1.5 text-xs font-black text-white ${
product.productId === 'points_60'
? 'bg-zinc-500'
: 'bg-[#ff4056]'
}`}
>
{product.badgeLabel}
</div>
<div className="px-2 py-3">
<div className="text-xl font-black">
{product.pointsAmount}
</div>
<div className="mt-1 text-xs text-zinc-500">
{formatRechargePrice(product.priceCents)}
</div>
<div className="my-2 h-px bg-zinc-100" />
<div className="text-sm text-zinc-800">
{isSubmitting === product.productId
? '处理中'
: product.description}
</div>
</div>
</button>
))}
</div>
) : (
<>
<div className="mt-5 grid grid-cols-3 gap-3">
{visibleProducts.map((product) => (
<button
type="button"
key={product.productId}
disabled={Boolean(isSubmitting)}
onClick={() => onSelectProduct(product)}
className="min-h-[6rem] rounded-xl border border-zinc-200 bg-zinc-50 px-2 py-4 text-center transition hover:border-[#ff4056] disabled:opacity-70"
>
<div className="text-lg font-black">{product.title}</div>
<div className="mt-2 text-xl font-black text-[#ff4056]">
{formatRechargePrice(product.priceCents)}
</div>
</button>
))}
</div>
<div className="mt-5 overflow-hidden rounded-xl border border-zinc-200">
<div className="border-b border-zinc-200 px-4 py-3 text-sm font-black">
</div>
<div className="grid grid-cols-5 text-center text-sm">
{center?.benefits.map((benefit) => (
<div key={benefit.benefitName} className="contents">
<div className="border-b border-zinc-100 bg-zinc-50 px-2 py-3 text-left text-zinc-600">
{benefit.benefitName}
</div>
<div className="border-b border-zinc-100 px-2 py-3 text-zinc-500">
{benefit.normalValue}
</div>
<div className="border-b border-zinc-100 px-2 py-3 font-bold text-emerald-700">
{benefit.monthValue}
</div>
<div className="border-b border-zinc-100 px-2 py-3 font-bold text-rose-500">
{benefit.seasonValue}
</div>
<div className="border-b border-zinc-100 px-2 py-3 font-bold text-amber-600">
{benefit.yearValue}
</div>
</div>
))}
</div>
</div>
</>
)}
</div>
</div>
</div>
);
}
export function RpgEntryHomeView({
activeTab,
onTabChange,
hasSavedGame,
savedSnapshot,
saveEntries,
saveError,
featuredEntries,
@@ -840,9 +1035,7 @@ export function RpgEntryHomeView({
isResumingSaveWorldKey,
platformError,
dashboardError,
onContinueGame,
onResumeSave,
onOpenCreateWorld,
onOpenCreateTypePicker,
onOpenGalleryDetail,
onOpenLibraryDetail,
@@ -851,10 +1044,21 @@ export function RpgEntryHomeView({
onSearchPublicCode,
isSearchingPublicCode = false,
onOpenProfileDashboardCard,
onRechargeSuccess,
createTabContent,
}: RpgEntryHomeViewProps) {
const authUi = useAuthUi();
const [desktopSearchKeyword, setDesktopSearchKeyword] = useState('');
const [isRechargeOpen, setIsRechargeOpen] = useState(false);
const [rechargeTab, setRechargeTab] = useState<'points' | 'membership'>(
'points',
);
const [rechargeCenter, setRechargeCenter] =
useState<ProfileRechargeCenterResponse | null>(null);
const [rechargeError, setRechargeError] = useState<string | null>(null);
const [isLoadingRecharge, setIsLoadingRecharge] = useState(false);
const [submittingRechargeProductId, setSubmittingRechargeProductId] =
useState<string | null>(null);
const [selectedCategoryTag, setSelectedCategoryTag] = useState<string | null>(
null,
);
@@ -879,19 +1083,6 @@ export function RpgEntryHomeView({
: ['home', 'create', 'category'],
[isAuthenticated],
);
const snapshotWorldName =
savedSnapshot?.gameState.customWorldProfile?.name ??
savedSnapshot?.gameState.currentScenePreset?.name ??
'继续冒险';
const snapshotCharacterName =
savedSnapshot?.gameState.playerCharacter?.title ??
savedSnapshot?.gameState.playerCharacter?.name ??
'旅人';
const snapshotDigest =
savedSnapshot?.gameState.storyEngineMemory?.continueGameDigest ??
savedSnapshot?.currentStory?.text ??
savedSnapshot?.gameState.customWorldProfile?.summary ??
'上一次冒险已经保存,可以从这里继续推进故事。';
const publicUserCode = buildPublicUserCode(authUi?.user);
const avatarLabel = getUserAvatarLabel(authUi?.user);
const remainingNarrativeCoins = profileDashboard?.walletBalance ?? 0;
@@ -941,6 +1132,36 @@ export function RpgEntryHomeView({
}
authUi?.openLoginModal();
};
const openRechargePanel = () => {
setIsRechargeOpen(true);
setRechargeError(null);
setIsLoadingRecharge(true);
void getRpgProfileRechargeCenter()
.then(setRechargeCenter)
.catch((error: unknown) => {
setRechargeCenter(null);
setRechargeError(
error instanceof Error ? error.message : '读取账户充值失败',
);
})
.finally(() => setIsLoadingRecharge(false));
};
const submitRechargeProduct = (product: ProfileRechargeProduct) => {
if (submittingRechargeProductId) {
return;
}
setSubmittingRechargeProductId(product.productId);
setRechargeError(null);
void createRpgProfileRechargeOrder(product.productId)
.then((response) => {
setRechargeCenter(response.center);
void onRechargeSuccess?.();
})
.catch((error: unknown) => {
setRechargeError(error instanceof Error ? error.message : '充值失败');
})
.finally(() => setSubmittingRechargeProductId(null));
};
const submitDesktopSearch = () => {
const keyword = desktopSearchKeyword.trim();
if (!keyword || !onSearchPublicCode || isSearchingPublicCode) {
@@ -964,44 +1185,46 @@ export function RpgEntryHomeView({
const categoryPageClass = isDesktopLayout
? DESKTOP_PAGE_STAGE_CLASS
: MOBILE_PAGE_STAGE_CLASS;
const leadPublicEntry = featuredShelf[0] ?? latestEntries[0] ?? null;
const openLeadPublicEntry = () => {
if (leadPublicEntry) {
onOpenGalleryDetail(leadPublicEntry);
return;
}
onTabChange('category');
};
const mobileHomeContent: ReactNode = (
<div className={`${MOBILE_PAGE_STAGE_CLASS} platform-mobile-home-stage`}>
<button
type="button"
onClick={() => {
if (hasSavedGame) {
onContinueGame();
return;
}
onOpenCreateWorld();
}}
onClick={openLeadPublicEntry}
className={`${HERO_SURFACE_CLASS} relative block w-full overflow-hidden px-4 py-4 text-left`}
>
<div className="absolute inset-0 bg-[var(--platform-hero-overlay-strong)]" />
<div className="relative z-10 flex min-h-[10rem] flex-col justify-between">
<div className="flex min-w-0 flex-wrap items-start justify-between gap-2">
<span className="platform-pill platform-pill--warm shrink-0">
{hasSavedGame ? 'CONTINUE' : 'CREATE'}
</span>
<div className="platform-mobile-hero-secondary platform-pill platform-pill--neutral max-w-full px-3 text-[11px] tracking-[0.08em]">
{hasSavedGame ? '继续冒险' : '创建世界'}
{leadPublicEntry
? describePublicGalleryCardKind(leadPublicEntry)
: '作品广场'}
</div>
</div>
<div className="min-w-0">
<div className="break-all text-[clamp(1.6rem,7.4vw,1.92rem)] font-black leading-tight text-white">
{hasSavedGame ? snapshotWorldName : '写下一个能被游玩的世界'}
{leadPublicEntry?.worldName ?? '浏览玩家作品'}
</div>
<div className="mt-2 max-w-[28rem] break-all text-sm leading-6 text-zinc-200/88">
{hasSavedGame
? `${snapshotCharacterName} 的进度已保存,点这里回到上一次停下来的故事节点。`
: '从设定、角色到场景网络,先生成一版可玩的世界底稿,再继续精修和发布。'}
{leadPublicEntry?.summaryText ||
leadPublicEntry?.subtitle ||
'从公开广场进入作品详情,挑一个世界开始游玩。'}
</div>
<div className="mt-4 flex min-w-0 items-center gap-2 text-sm font-semibold text-white/90">
<span className="min-w-0 break-all">
{hasSavedGame ? '继续推进故事' : '进入创作工作台'}
</span>
<span className="min-w-0 break-all"></span>
<ArrowRight className="h-4 w-4" />
</div>
</div>
@@ -1020,18 +1243,18 @@ export function RpgEntryHomeView({
<EmptyShelf text="正在读取精选作品..." />
) : featuredShelf.length > 0 ? (
<div className="flex min-w-0 gap-3 overflow-x-auto pb-1 scrollbar-hide">
{featuredShelf.map((entry: CustomWorldGalleryCard) => (
{featuredShelf.map((entry: PlatformPublicGalleryCard) => (
<WorldCard
key={`${entry.ownerUserId}:${entry.profileId}:featured`}
key={`${buildPublicGalleryCardKey(entry)}:featured`}
entry={entry}
badge="推荐"
metaLabel={describePlatformThemeLabel(entry.themeMode)}
metaLabel={describePublicGalleryCardKind(entry)}
onClick={() => onOpenGalleryDetail(entry)}
/>
))}
</div>
) : (
<EmptyShelf text="还没有公开作品,先创建你的第一个世界吧。" />
<EmptyShelf text="公开广场暂时还没有精选作品。" />
)}
</section>
@@ -1041,9 +1264,9 @@ export function RpgEntryHomeView({
<EmptyShelf text="正在读取最新发布..." />
) : latestEntries.length > 0 ? (
<div className="flex min-w-0 gap-3 overflow-x-auto pb-1 scrollbar-hide">
{latestEntries.map((entry: CustomWorldGalleryCard) => (
{latestEntries.map((entry: PlatformPublicGalleryCard) => (
<WorldCard
key={`${entry.ownerUserId}:${entry.profileId}:latest`}
key={`${buildPublicGalleryCardKey(entry)}:latest`}
entry={entry}
badge={formatPlatformWorldTime(entry.publishedAt)}
metaLabel={entry.authorDisplayName}
@@ -1086,7 +1309,7 @@ export function RpgEntryHomeView({
<div className="mt-4 grid grid-cols-2 gap-2.5 sm:gap-3 lg:grid-cols-3 xl:grid-cols-4">
{activeCategoryGroup.entries.map((entry) => (
<WorldCard
key={`${entry.ownerUserId}:${entry.profileId}:category:${activeCategoryGroup.tag}`}
key={`${buildPublicGalleryCardKey(entry)}:category:${activeCategoryGroup.tag}`}
entry={entry}
badge={activeCategoryGroup.tag}
metaLabel={entry.authorDisplayName}
@@ -1142,7 +1365,11 @@ export function RpgEntryHomeView({
key={`${entry.ownerUserId}:${entry.profileId}:mine`}
entry={entry}
onClick={() => onOpenLibraryDetail(entry)}
onDelete={onDeleteLibraryEntry ? () => onDeleteLibraryEntry(entry) : undefined}
onDelete={
onDeleteLibraryEntry
? () => onDeleteLibraryEntry(entry)
: undefined
}
isDeleting={deletingLibraryEntryId === entry.profileId}
/>
),
@@ -1267,12 +1494,17 @@ export function RpgEntryHomeView({
<button
type="button"
onClick={openRechargePanel}
className="platform-profile-action flex shrink-0 items-center gap-2 rounded-[1.1rem] px-3 py-2 text-left"
>
<Crown className="h-4 w-4" />
<div>
<div className="text-xs font-bold"></div>
<div className="text-[10px] opacity-80"></div>
<div className="text-[10px] opacity-80">
{rechargeCenter?.membership.status === 'active'
? '叙世会员'
: '普通用户'}
</div>
</div>
<ChevronRight className="h-4 w-4 opacity-80" />
</button>
@@ -1406,14 +1638,7 @@ export function RpgEntryHomeView({
<div className="grid gap-5 xl:grid-cols-[minmax(0,1.55fr)_22rem]">
<button
type="button"
onClick={() => {
if (hasSavedGame) {
onContinueGame();
return;
}
onOpenCreateWorld();
}}
onClick={openLeadPublicEntry}
className={`${HERO_SURFACE_CLASS} relative block overflow-hidden px-7 py-6 text-left`}
>
{desktopHeroCover ? (
@@ -1427,27 +1652,25 @@ export function RpgEntryHomeView({
<div className="absolute inset-0 bg-[var(--platform-hero-overlay-strong)]" />
<div className="relative z-10 flex min-h-[24rem] flex-col justify-between">
<div className="flex items-start justify-between gap-4">
<span className="platform-pill platform-pill--warm">
{hasSavedGame ? 'CONTINUE STORY' : 'CREATE WORLD'}
</span>
<span className="platform-pill platform-pill--warm"></span>
<span className="platform-pill platform-pill--neutral px-3">
{hasSavedGame ? '最近存档' : '创作入口'}
{leadPublicEntry
? describePublicGalleryCardKind(leadPublicEntry)
: '作品广场'}
</span>
</div>
<div className="max-w-[35rem]">
<div className="text-5xl font-semibold leading-[1.08] text-white">
{hasSavedGame
? snapshotWorldName
: '把你的世界观直接变成可游玩的舞台'}
{leadPublicEntry?.worldName ?? '浏览玩家作品'}
</div>
<div className="mt-4 text-base leading-8 text-zinc-200/86">
{hasSavedGame
? `${snapshotCharacterName} 的进度已经保存,桌面端可以直接从这里回到上一次停下来的关键节点。`
: '从设定、角色、世界结构到可玩流程,一次生成创作底稿,再继续精修并发布到平台广场。'}
{leadPublicEntry?.summaryText ||
leadPublicEntry?.subtitle ||
'从公开广场进入作品详情,挑一个世界开始游玩。'}
</div>
<div className="mt-5 inline-flex items-center gap-2 rounded-full border border-white/18 bg-white/18 px-4 py-2 text-sm font-semibold text-white/92">
<span>{hasSavedGame ? '继续推进故事' : '进入创作工作台'}</span>
<span></span>
<ArrowRight className="h-4 w-4" />
</div>
</div>
@@ -1499,7 +1722,7 @@ export function RpgEntryHomeView({
<div className="space-y-3">
{desktopTrendingEntries.map((entry, index) => (
<DesktopTrendingItem
key={`${entry.ownerUserId}:${entry.profileId}:desktop-trend`}
key={`${buildPublicGalleryCardKey(entry)}:desktop-trend`}
entry={entry}
rank={index + 1}
onClick={() => onOpenGalleryDetail(entry)}
@@ -1521,70 +1744,39 @@ export function RpgEntryHomeView({
<div className="grid gap-4 xl:grid-cols-2">
{desktopFeaturedGrid.map((entry) => (
<WorldCard
key={`${entry.ownerUserId}:${entry.profileId}:desktop-featured`}
key={`${buildPublicGalleryCardKey(entry)}:desktop-featured`}
entry={entry}
badge="推荐"
metaLabel={describePlatformThemeLabel(entry.themeMode)}
metaLabel={describePublicGalleryCardKind(entry)}
onClick={() => onOpenGalleryDetail(entry)}
className="h-[16rem] w-full min-w-0"
/>
))}
</div>
) : (
<EmptyShelf text="还没有公开作品,先创建你的第一个世界吧。" />
<EmptyShelf text="公开广场暂时还没有精选作品。" />
)}
</section>
<section className="platform-desktop-panel px-5 py-5">
<SectionHeader
title={hasSavedGame ? '继续进度' : '创作入口'}
title={
desktopLibraryPreview.length > 0
? '最近作品'
: historyEntries.length > 0
? '最近浏览'
: '作品广场'
}
detail="QUICK ACCESS"
/>
<button
type="button"
onClick={() => {
if (hasSavedGame) {
onContinueGame();
return;
}
onOpenCreateWorld();
}}
className="platform-surface platform-surface--soft platform-interactive-card relative block w-full overflow-hidden px-5 py-5 text-left"
>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(255,164,198,0.18),transparent_28%),radial-gradient(circle_at_bottom_left,rgba(255,202,176,0.18),transparent_32%)]" />
<div className="relative z-10">
<div className="flex items-center justify-between gap-3">
<span className="platform-pill platform-pill--cool">
{hasSavedGame ? 'SAVE POINT' : 'START HERE'}
</span>
<ArrowRight className="h-4 w-4 text-[var(--platform-text-soft)]" />
</div>
<div className="mt-4 text-2xl font-semibold text-[var(--platform-text-strong)]">
{hasSavedGame ? snapshotWorldName : '从这里开启新的创作'}
</div>
<div className="mt-2 text-sm leading-7 text-[color:color-mix(in_srgb,var(--platform-text-base)_84%,transparent)]">
{hasSavedGame
? `当前角色:${snapshotCharacterName}`
: '快速进入自定义世界创作,继续补齐设定、角色与核心冲突。'}
</div>
<div className="mt-3 line-clamp-3 text-sm leading-6 text-[var(--platform-text-soft)]">
{hasSavedGame
? snapshotDigest
: '先生成一版可玩的世界底稿,再继续编辑并发布。'}
</div>
</div>
</button>
<div className="mt-5">
<div>
<div className="text-[10px] font-semibold tracking-[0.24em] text-[var(--platform-text-soft)]">
{desktopLibraryPreview.length > 0
? '最近作品'
: historyEntries.length > 0
? '最近浏览'
: isAuthenticated
? '创作状态'
: '账户状态'}
: '公开作品'}
</div>
{desktopLibraryPreview.length > 0 ? (
@@ -1655,9 +1847,7 @@ export function RpgEntryHomeView({
</div>
) : (
<div className="platform-subpanel mt-3 rounded-[1.35rem] px-4 py-4 text-sm leading-6 text-[var(--platform-text-base)]">
{isAuthenticated
? '创建一个草稿后,这里会出现你最近保存的作品。'
: '登录后可同步你的创作、游玩进度与平台资料。'}
广
</div>
)}
</div>
@@ -1672,7 +1862,7 @@ export function RpgEntryHomeView({
<div className="grid gap-4 xl:grid-cols-3">
{desktopReleaseGrid.map((entry) => (
<WorldCard
key={`${entry.ownerUserId}:${entry.profileId}:desktop-latest`}
key={`${buildPublicGalleryCardKey(entry)}:desktop-latest`}
entry={entry}
badge={formatPlatformWorldTime(entry.publishedAt)}
metaLabel={entry.authorDisplayName}
@@ -1722,7 +1912,7 @@ export function RpgEntryHomeView({
}}
>
<div
className={`platform-bottom-nav grid ${visibleTabs.length === 5 ? 'grid-cols-5' : 'grid-cols-3'}`}
className={`platform-bottom-nav grid ${visibleTabs.length === 5 ? 'grid-cols-5' : visibleTabs.length === 4 ? 'grid-cols-4' : 'grid-cols-2'}`}
>
{visibleTabs.map((tab) => (
<PlatformTabButton
@@ -1736,6 +1926,18 @@ export function RpgEntryHomeView({
))}
</div>
</div>
{isRechargeOpen ? (
<AccountRechargeModal
center={rechargeCenter}
activeTab={rechargeTab}
isLoading={isLoadingRecharge}
isSubmitting={submittingRechargeProductId}
error={rechargeError}
onTabChange={setRechargeTab}
onClose={() => setIsRechargeOpen(false)}
onSelectProduct={submitRechargeProduct}
/>
) : null}
</div>
);
}
@@ -1751,7 +1953,9 @@ export function RpgEntryHomeView({
<Search className="h-4 w-4 shrink-0" />
<input
value={desktopSearchKeyword}
onChange={(event) => setDesktopSearchKeyword(event.target.value)}
onChange={(event) =>
setDesktopSearchKeyword(event.target.value)
}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault();
@@ -1804,7 +2008,7 @@ export function RpgEntryHomeView({
{authUi?.user?.displayName || '进入账户'}
</span>
<span className="block truncate text-xs text-[var(--platform-text-soft)]">
{authUi?.user ? publicUserCode : '登录后同步作与进度'}
{authUi?.user ? publicUserCode : '登录后同步作与进度'}
</span>
</span>
</button>
@@ -1831,6 +2035,18 @@ export function RpgEntryHomeView({
</div>
</div>
</div>
{isRechargeOpen ? (
<AccountRechargeModal
center={rechargeCenter}
activeTab={rechargeTab}
isLoading={isLoadingRecharge}
isSubmitting={submittingRechargeProductId}
error={rechargeError}
onTabChange={setRechargeTab}
onClose={() => setIsRechargeOpen(false)}
onSelectProduct={submitRechargeProduct}
/>
) : null}
</div>
);
}

View File

@@ -48,8 +48,9 @@ export function createFailedRpgEntryAgentOperation(params: {
status: 'failed',
phaseLabel: params.phaseLabel,
phaseDetail: params.error,
progress: 100,
progress: 0,
error: params.error,
updatedAt: new Date().toISOString(),
};
}

View File

@@ -2,13 +2,35 @@ import type {
CustomWorldGalleryCard,
CustomWorldLibraryEntry,
} from '../../../packages/shared/src/contracts/runtime';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
import { resolveCustomWorldCampSceneImage } from '../../data/customWorldVisuals';
import type { CustomWorldProfile } from '../../types';
export type PlatformWorldCardLike =
| CustomWorldGalleryCard
| CustomWorldLibraryEntry<CustomWorldProfile>;
| CustomWorldLibraryEntry<CustomWorldProfile>
| PlatformPuzzleGalleryCard;
export type PlatformPuzzleGalleryCard = {
sourceType: 'puzzle';
workId: string;
profileId: string;
ownerUserId: string;
authorDisplayName: string;
worldName: string;
subtitle: string;
summaryText: string;
coverImageSrc: string | null;
themeTags: string[];
visibility: 'published';
publishedAt: string | null;
updatedAt: string;
};
export type PlatformPublicGalleryCard =
| CustomWorldGalleryCard
| PlatformPuzzleGalleryCard;
export function isLibraryWorldEntry(
entry: PlatformWorldCardLike,
@@ -16,6 +38,32 @@ export function isLibraryWorldEntry(
return 'profile' in entry;
}
export function isPuzzleGalleryEntry(
entry: PlatformWorldCardLike,
): entry is PlatformPuzzleGalleryCard {
return 'sourceType' in entry && entry.sourceType === 'puzzle';
}
export function mapPuzzleWorkToPlatformGalleryCard(
work: PuzzleWorkSummary,
): PlatformPuzzleGalleryCard {
return {
sourceType: 'puzzle',
workId: work.workId,
profileId: work.profileId,
ownerUserId: work.ownerUserId,
authorDisplayName: work.authorDisplayName,
worldName: work.levelName,
subtitle: '拼图关卡',
summaryText: work.summary,
coverImageSrc: work.coverImageSrc,
themeTags: work.themeTags,
visibility: 'published',
publishedAt: work.publishedAt,
updatedAt: work.updatedAt,
};
}
export function resolvePlatformWorldCoverImage(entry: PlatformWorldCardLike) {
if (entry.coverImageSrc) {
return entry.coverImageSrc;
@@ -37,6 +85,10 @@ export function resolvePlatformWorldLeadPortrait(entry: PlatformWorldCardLike) {
}
export function buildPlatformWorldTags(entry: PlatformWorldCardLike) {
if (isPuzzleGalleryEntry(entry)) {
return entry.themeTags.length > 0 ? entry.themeTags.slice(0, 3) : ['拼图'];
}
if (!isLibraryWorldEntry(entry)) {
return [
describePlatformThemeLabel(entry.themeMode),
@@ -71,7 +123,7 @@ export function formatPlatformWorldTime(value: string | null) {
}
export function describePlatformThemeLabel(
themeMode: PlatformWorldCardLike['themeMode'],
themeMode: CustomWorldGalleryCard['themeMode'],
) {
switch (themeMode) {
case 'martial':

View File

@@ -218,60 +218,27 @@ export function useRpgCreationResultAutosave(
} satisfies SyncedAgentDraftResult;
}
const { operation } = await executeRpgCreationAction(
activeAgentSessionId,
{
action: 'sync_result_profile',
profile: normalizedProfile as unknown as Record<string, unknown>,
},
// Agent 结果页不再把前端 profile 回写到 session。
// session.draftProfile 是真相源;这里只刷新后端最新快照,避免在采集/生成早期误触 sync_result_profile。
const latestSession = await syncAgentSessionSnapshot(activeAgentSessionId);
const latestProfile = normalizeAgentBackedProfile(
buildDraftResultProfile(latestSession) ?? profile,
);
setAgentOperation(operation);
persistAgentUiState(activeAgentSessionId, operation.operationId);
for (let attempt = 0; attempt < 60; attempt += 1) {
const latestOperation = await getRpgCreationOperation(
activeAgentSessionId,
operation.operationId,
);
setAgentOperation(latestOperation);
if (latestOperation.status === 'failed') {
throw new Error(
latestOperation.error ||
latestOperation.phaseDetail ||
'同步结果页世界快照失败。',
);
}
if (latestOperation.status === 'completed') {
persistAgentUiState(activeAgentSessionId, null);
const latestSession = await syncAgentSessionSnapshot(
activeAgentSessionId,
);
const latestProfile = normalizeAgentBackedProfile(
buildDraftResultProfile(latestSession) ?? profile,
);
if (latestProfile) {
setGeneratedCustomWorldProfile(latestProfile);
}
latestAgentResultSyncSignatureRef.current = profileSignature;
return {
session: latestSession,
profile: latestProfile,
} satisfies SyncedAgentDraftResult;
}
await new Promise((resolve) => window.setTimeout(resolve, 200));
if (latestProfile) {
setGeneratedCustomWorldProfile(latestProfile);
}
latestAgentResultSyncSignatureRef.current =
stringifyAgentBackedProfile(latestProfile);
throw new Error('同步结果页世界快照超时。');
return {
session: latestSession,
profile: latestProfile,
} satisfies SyncedAgentDraftResult;
},
[
activeAgentSessionId,
agentSession,
buildDraftResultProfile,
persistAgentUiState,
setAgentOperation,
setGeneratedCustomWorldProfile,
syncAgentSessionSnapshot,
],

View File

@@ -0,0 +1,278 @@
/** @vitest-environment jsdom */
import { act, render } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { CustomWorldAgentSessionSnapshot } from '../../../packages/shared/src/contracts/customWorldAgent';
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldWorkSummary';
import { WorldType, type CustomWorldProfile } from '../../types';
import {
executeRpgCreationAction,
upsertRpgWorldProfile,
} from '../../services/rpg-creation';
import { useRpgCreationResultAutosave } from './useRpgCreationResultAutosave';
import { useRpgEntryLibraryDetail } from './useRpgEntryLibraryDetail';
vi.mock('../../services/rpg-creation', () => ({
executeRpgCreationAction: vi.fn(),
getRpgCreationOperation: vi.fn(),
upsertRpgWorldProfile: vi.fn(),
}));
vi.mock('../../services/rpg-entry', () => ({
deleteRpgEntryWorldProfile: vi.fn(),
getRpgEntryWorldGalleryDetail: vi.fn(),
listRpgEntryWorldLibrary: vi.fn(),
publishRpgEntryWorldProfile: vi.fn(),
unpublishRpgEntryWorldProfile: vi.fn(),
}));
function buildProfile(name: string): CustomWorldProfile {
return {
id: `profile-${name}`,
settingText: name,
name,
subtitle: name,
summary: name,
tone: '测试',
playerGoal: '测试',
templateWorldType: WorldType.WUXIA,
compatibilityTemplateWorldType: WorldType.WUXIA,
majorFactions: [],
coreConflicts: [],
attributeSchema: {
id: `schema-${name}`,
worldId: `profile-${name}`,
schemaVersion: 1,
generatedFrom: {
worldType: WorldType.CUSTOM,
worldName: name,
settingSummary: name,
tone: '测试',
conflictCore: '测试',
},
slots: [],
},
playableNpcs: [],
storyNpcs: [],
items: [],
landmarks: [],
generationMode: 'full',
generationStatus: 'complete',
};
}
function buildSession(
overrides: Partial<CustomWorldAgentSessionSnapshot> = {},
): CustomWorldAgentSessionSnapshot {
return {
sessionId: 'agent-session-1',
currentTurn: 1,
anchorContent: {
worldPromise: null,
playerFantasy: null,
themeBoundary: null,
playerEntryPoint: null,
coreConflict: null,
keyRelationships: [],
hiddenLines: null,
iconicElements: null,
},
progressPercent: 20,
lastAssistantReply: '继续补齐世界草稿。',
stage: 'clarifying',
focusCardId: null,
creatorIntent: null,
creatorIntentReadiness: {
isReady: false,
completedKeys: [],
missingKeys: [],
},
anchorPack: null,
lockState: null,
draftProfile: null,
messages: [],
draftCards: [],
pendingClarifications: [],
suggestedActions: [],
recommendedReplies: [],
qualityFindings: [],
assetCoverage: {
roleAssets: [],
sceneAssets: [],
allRoleAssetsReady: false,
allSceneAssetsReady: false,
},
resultPreview: null,
updatedAt: '2026-04-25T00:00:00.000Z',
...overrides,
};
}
describe('RPG Agent 草稿恢复', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('作品摘要已有对象数量但 session 没有 draftProfile 时恢复 Agent 页面', async () => {
const syncAgentSessionSnapshot = vi.fn(async () =>
buildSession({
stage: 'clarifying',
draftProfile: null,
}),
);
const setSelectionStage = vi.fn();
const persistAgentUiState = vi.fn();
const setGeneratedCustomWorldProfile = vi.fn();
const setCustomWorldResultViewSource = vi.fn();
const suppressAgentDraftResultAutoOpen = vi.fn();
let openWork:
| ((work: CustomWorldWorkSummary) => Promise<void>)
| null = null;
function Harness() {
openWork = useRpgEntryLibraryDetail({
userId: 'user-1',
selectedDetailEntry: null,
setSelectedDetailEntry: vi.fn(),
savedCustomWorldEntries: [],
setSavedCustomWorldEntries: vi.fn(),
setGeneratedCustomWorldProfile,
setCustomWorldError: vi.fn(),
setCustomWorldAutoSaveError: vi.fn(),
setCustomWorldAutoSaveState: vi.fn(),
setCustomWorldGenerationViewSource: vi.fn(),
setCustomWorldResultViewSource,
setSelectionStage,
setPlatformTabToCreate: vi.fn(),
setPlatformError: vi.fn(),
appendBrowseHistoryEntry: vi.fn(async () => {}),
refreshCustomWorldWorks: vi.fn(async () => []),
refreshPublishedGallery: vi.fn(async () => []),
persistAgentUiState,
syncAgentSessionSnapshot,
buildDraftResultProfile: (session) =>
(session?.draftProfile as CustomWorldProfile | null) ?? null,
suppressAgentDraftResultAutoOpen,
releaseAgentDraftResultAutoOpenSuppression: vi.fn(),
resetAutoSaveTrackingToIdle: vi.fn(),
markAutoSavedProfile: vi.fn(),
}).handleOpenCreationWork;
return null;
}
render(<Harness />);
await act(async () => {
await openWork?.({
workId: 'draft:agent-session-1',
sourceType: 'agent_session',
status: 'draft',
title: '未生成草稿作品',
subtitle: '',
summary: '',
updatedAt: '2026-04-25T00:00:00.000Z',
stage: 'clarifying',
stageLabel: '澄清中',
playableNpcCount: 2,
landmarkCount: 3,
sessionId: 'agent-session-1',
canResume: true,
canEnterWorld: false,
});
});
expect(syncAgentSessionSnapshot).toHaveBeenCalledWith('agent-session-1');
expect(suppressAgentDraftResultAutoOpen).toHaveBeenCalled();
expect(persistAgentUiState).toHaveBeenCalledWith('agent-session-1', null);
expect(setGeneratedCustomWorldProfile).toHaveBeenLastCalledWith(null);
expect(setCustomWorldResultViewSource).toHaveBeenLastCalledWith(null);
expect(setSelectionStage).toHaveBeenLastCalledWith('agent-workspace');
expect(setSelectionStage).not.toHaveBeenCalledWith('custom-world-result');
});
it('Agent 结果页自动保存只刷新 session draftProfile不触发 sync_result_profile', async () => {
const oldProfile = buildProfile('旧前端快照');
const latestProfile = {
...buildProfile('服务端草稿快照'),
summary: '自动保存应保存这份 session 最新草稿。',
};
const latestSession = buildSession({
stage: 'object_refining',
draftProfile: latestProfile as unknown as Record<string, unknown>,
});
const syncAgentSessionSnapshot = vi.fn(async () => latestSession);
vi.mocked(upsertRpgWorldProfile).mockResolvedValue({
entry: {
ownerUserId: 'user-1',
profileId: latestProfile.id,
publicWorkCode: null,
authorPublicUserCode: null,
profile: latestProfile,
visibility: 'draft',
publishedAt: null,
updatedAt: '2026-04-25T00:00:00.000Z',
authorDisplayName: '测试玩家',
worldName: latestProfile.name,
subtitle: latestProfile.subtitle,
summaryText: latestProfile.summary,
coverImageSrc: null,
themeMode: 'tide',
playableNpcCount: 0,
landmarkCount: 0,
},
entries: [],
});
function Harness() {
useRpgCreationResultAutosave({
selectionStage: 'custom-world-result',
activeAgentSessionId: 'agent-session-1',
agentSession: buildSession({
stage: 'object_refining',
draftProfile: oldProfile as unknown as Record<string, unknown>,
resultPreview: {
publishReady: false,
blockers: [],
qualityFindings: [],
sourceLabel: '旧预览',
} as never,
}),
generatedCustomWorldProfile: oldProfile,
isAgentDraftResultView: true,
userId: 'user-1',
setGeneratedCustomWorldProfile: vi.fn(),
setAgentOperation: vi.fn(),
setSavedCustomWorldEntries: vi.fn(),
setSelectedDetailEntry: vi.fn(),
refreshCustomWorldWorks: vi.fn(async () => []),
persistAgentUiState: vi.fn(),
syncAgentSessionSnapshot,
buildDraftResultProfile: (session) =>
(session?.draftProfile as CustomWorldProfile | null) ?? null,
});
return null;
}
vi.useFakeTimers();
render(<Harness />);
await act(async () => {
await vi.advanceTimersByTimeAsync(650);
});
vi.useRealTimers();
expect(syncAgentSessionSnapshot).toHaveBeenCalledWith('agent-session-1');
expect(upsertRpgWorldProfile).toHaveBeenCalledWith(latestProfile, {
sourceAgentSessionId: 'agent-session-1',
});
expect(
vi.mocked(executeRpgCreationAction).mock.calls.some(
([, payload]) => payload?.action === 'sync_result_profile',
),
).toBe(false);
});
});

View File

@@ -81,6 +81,14 @@ type UseRpgEntryLibraryDetailParams = {
markAutoSavedProfile: (profile: CustomWorldProfile) => void;
};
const AGENT_RESULT_STAGES = new Set([
'object_refining',
'visual_refining',
'long_tail_review',
'ready_to_publish',
'published',
]);
function isMissingRpgEntryAgentSessionError(error: unknown) {
return (
error instanceof ApiClientError &&
@@ -241,12 +249,13 @@ export function useRpgEntryLibraryDetail(
setCustomWorldResultViewSource(null);
resetAutoSaveTrackingToIdle();
const shouldOpenAgentWorkspace =
work.playableNpcCount <= 0 && work.landmarkCount <= 0;
try {
const latestSession = await syncAgentSessionSnapshot(work.sessionId);
const nextProfile = buildDraftResultProfile(latestSession);
const shouldOpenAgentWorkspace =
!latestSession?.draftProfile ||
!latestSession.stage ||
!AGENT_RESULT_STAGES.has(latestSession.stage);
const shouldResumeFailedGenerationView =
!nextProfile &&
@@ -268,8 +277,8 @@ export function useRpgEntryLibraryDetail(
return;
}
if (shouldOpenAgentWorkspace && !nextProfile) {
// 仅八锚点未整理成底稿时才恢复 Agent 对话工作区
if (shouldOpenAgentWorkspace) {
// 还没有服务端草稿真相源时只能恢复 Agent对象数量等摘要字段不能决定结果页入口
suppressAgentDraftResultAutoOpen();
persistAgentUiState(work.sessionId, null);
setGeneratedCustomWorldProfile(null);

View File

@@ -1,11 +1,15 @@
import { lazy, Suspense } from 'react';
import { lazy, Suspense, useEffect } from 'react';
import { normalizePlayerProgressionState } from '../../data/playerProgression';
import {
APP_RUNTIME_ROUTES,
pushAppHistoryPath,
} from '../../routing/appPageRoutes';
import { UI_CHROME } from '../../uiAssets';
import { useAuthUi } from '../auth/AuthUiContext';
import { RpgRuntimeCanvasStage } from './RpgRuntimeCanvasStage';
import type { RpgRuntimeShellProps as RpgRuntimeShellComponentProps } from './types';
import { RpgRuntimeStageRouter } from './RpgRuntimeStageRouter';
import type { RpgRuntimeShellProps as RpgRuntimeShellComponentProps } from './types';
import { useRpgRuntimeShellViewModel } from './useRpgRuntimeShellViewModel';
const RpgRuntimeOverlayHost = lazy(async () => {
@@ -121,6 +125,22 @@ export function RpgRuntimeShell({
),
);
useEffect(() => {
if (gameState.worldType && !gameState.playerCharacter) {
pushAppHistoryPath(APP_RUNTIME_ROUTES['rpg-character-select']);
return;
}
if (visibleGameState.playerCharacter && visibleCurrentStory) {
pushAppHistoryPath(APP_RUNTIME_ROUTES['rpg-adventure']);
}
}, [
gameState.playerCharacter,
gameState.worldType,
visibleCurrentStory,
visibleGameState.playerCharacter,
]);
return (
<div
className={`${isPlatformShell ? `platform-ui-shell platform-theme ${platformThemeClass} text-[var(--platform-text-strong)]` : 'fusion-pixel-app pixel-root-shell text-zinc-100'} flex h-screen max-h-screen flex-col overflow-hidden font-sans`}

View File

@@ -1,5 +1,10 @@
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import {
pushAppHistoryPath,
resolvePathForSelectionStage,
resolveSelectionStageFromPath,
} from '../../routing/appPageRoutes';
import type { GameState } from '../../types';
import type { GameCanvasEntitySelection } from '../GameCanvas';
import type { SelectionStage } from '../platform-entry';
@@ -34,11 +39,28 @@ export function useRpgRuntimeOverlayState(params: {
characterChatModalOpen,
hasNpcModalOpen,
} = params;
const [selectionStage, setSelectionStage] = useState<SelectionStage>('platform');
const [selectionStage, setRawSelectionStage] = useState<SelectionStage>(() =>
resolveSelectionStageFromPath(window.location.pathname),
);
const [overlayPanel, setOverlayPanel] = useState<OverlayPanel>(null);
const [selectedSceneEntity, setSelectedSceneEntity] =
useState<GameCanvasEntitySelection | null>(null);
const [showTeamModal, setShowTeamModal] = useState(false);
const setSelectionStage = useCallback((stage: SelectionStage) => {
setRawSelectionStage(stage);
pushAppHistoryPath(resolvePathForSelectionStage(stage));
}, []);
useEffect(() => {
const syncStageFromHistory = () => {
setRawSelectionStage(
resolveSelectionStageFromPath(window.location.pathname),
);
};
window.addEventListener('popstate', syncStageFromHistory);
return () => window.removeEventListener('popstate', syncStageFromHistory);
}, []);
useEffect(() => {
setSelectedSceneEntity(null);

View File

@@ -660,7 +660,8 @@ body {
);
}
.platform-surface > * {
/* 背景图和遮罩必须保持绝对定位,避免 banner 图进入普通流后撑开首页布局。 */
.platform-surface > :not(.absolute) {
position: relative;
z-index: 1;
}

View File

@@ -0,0 +1,48 @@
import { describe, expect, it } from 'vitest';
import {
APP_RUNTIME_ROUTES,
isKnownMainAppPagePath,
normalizeAppPath,
resolvePathForSelectionStage,
resolveSelectionStageFromPath,
} from './appPageRoutes';
describe('appPageRoutes', () => {
it('normalizes page paths for stable matching', () => {
expect(normalizeAppPath('')).toBe('/');
expect(normalizeAppPath('/CREATION/RPG/AGENT/')).toBe('/creation/rpg/agent');
});
it('resolves platform entry stages from independent paths', () => {
expect(resolveSelectionStageFromPath('/creation/rpg/agent')).toBe(
'agent-workspace',
);
expect(resolveSelectionStageFromPath('/creation/big-fish/result/')).toBe(
'big-fish-result',
);
expect(resolveSelectionStageFromPath('/gallery/puzzle/detail')).toBe(
'puzzle-gallery-detail',
);
});
it('falls back to platform for unknown paths inside the main app', () => {
expect(resolveSelectionStageFromPath('/missing')).toBe('platform');
});
it('resolves paths from selection stages', () => {
expect(resolvePathForSelectionStage('custom-world-generating')).toBe(
'/creation/rpg/generating',
);
expect(resolvePathForSelectionStage('puzzle-runtime')).toBe(
'/runtime/puzzle',
);
});
it('recognizes runtime pages as main app pages', () => {
expect(
isKnownMainAppPagePath(APP_RUNTIME_ROUTES['rpg-character-select']),
).toBe(true);
expect(isKnownMainAppPagePath('/runtime/rpg/adventure/')).toBe(true);
});
});

View File

@@ -0,0 +1,69 @@
import type { SelectionStage } from '../components/platform-entry';
export type RuntimePageRoute = 'rpg-character-select' | 'rpg-adventure';
const STAGE_ROUTE_ENTRIES = [
['platform', '/'],
['detail', '/worlds/detail'],
['agent-workspace', '/creation/rpg/agent'],
['custom-world-generating', '/creation/rpg/generating'],
['custom-world-result', '/creation/rpg/result'],
['big-fish-agent-workspace', '/creation/big-fish/agent'],
['big-fish-result', '/creation/big-fish/result'],
['big-fish-runtime', '/runtime/big-fish'],
['puzzle-agent-workspace', '/creation/puzzle/agent'],
['puzzle-result', '/creation/puzzle/result'],
['puzzle-gallery-detail', '/gallery/puzzle/detail'],
['puzzle-runtime', '/runtime/puzzle'],
] as const satisfies readonly (readonly [SelectionStage, string])[];
export const APP_STAGE_ROUTES: Record<SelectionStage, string> =
Object.fromEntries(STAGE_ROUTE_ENTRIES) as Record<SelectionStage, string>;
export const APP_RUNTIME_ROUTES: Record<RuntimePageRoute, string> = {
'rpg-character-select': '/runtime/rpg/characters',
'rpg-adventure': '/runtime/rpg/adventure',
};
const ROUTE_STAGE_BY_PATH = new Map(
STAGE_ROUTE_ENTRIES.map(([stage, path]) => [path, stage] as const),
) as Map<string, SelectionStage>;
export function normalizeAppPath(pathname: string) {
const trimmedPathname = pathname.trim().toLowerCase();
if (!trimmedPathname || trimmedPathname === '/') {
return '/';
}
return trimmedPathname.replace(/\/+$/u, '');
}
export function resolveSelectionStageFromPath(
pathname: string,
): SelectionStage {
return ROUTE_STAGE_BY_PATH.get(normalizeAppPath(pathname)) ?? 'platform';
}
export function resolvePathForSelectionStage(stage: SelectionStage) {
return APP_STAGE_ROUTES[stage] ?? APP_STAGE_ROUTES.platform;
}
export function isKnownMainAppPagePath(pathname: string) {
const normalizedPath = normalizeAppPath(pathname);
const runtimePaths: readonly string[] = Object.values(APP_RUNTIME_ROUTES);
return (
ROUTE_STAGE_BY_PATH.has(normalizedPath) ||
runtimePaths.includes(normalizedPath)
);
}
export function pushAppHistoryPath(path: string) {
const normalizedPath = normalizeAppPath(path);
if (normalizeAppPath(window.location.pathname) === normalizedPath) {
return;
}
// 页面阶段变化是用户可感知导航,写入 history 以支持前进后退。
window.history.pushState(null, '', normalizedPath);
}

View File

@@ -33,6 +33,18 @@ describe('matchAppRoute', () => {
});
});
it('routes independent page paths to the main app shell', () => {
expect(matchAppRoute('/creation/rpg/agent')).toEqual({
kind: 'game',
});
expect(matchAppRoute('/runtime/puzzle')).toEqual({
kind: 'game',
});
expect(matchAppRoute('/runtime/big-fish')).toEqual({
kind: 'game',
});
});
it('does not treat unrelated prefixes as preset editor routes', () => {
expect(matchAppRoute('/npc-editorial')).toEqual({
kind: 'game',

View File

@@ -2,6 +2,8 @@
import { type ComponentType, lazy, type LazyExoticComponent } from 'react';
import { normalizeAppPath } from './appPageRoutes';
type AppRouteComponent = LazyExoticComponent<
ComponentType<Record<string, unknown>>
>;
@@ -30,13 +32,7 @@ const BigFishPlaygroundApp = lazy(() => import('../BigFishPlaygroundApp')) as Ap
const PuzzlePlaygroundApp = lazy(() => import('../PuzzlePlaygroundApp')) as AppRouteComponent;
function normalizeRoutePath(pathname: string) {
const trimmedPathname = pathname.trim().toLowerCase();
if (!trimmedPathname || trimmedPathname === '/') {
return '/';
}
return trimmedPathname.replace(/\/+$/u, '');
return normalizeAppPath(pathname);
}
export function matchAppRoute(pathname: string): AppRouteMatch {

View File

@@ -6,76 +6,45 @@ import type {
ExecuteBigFishActionRequest,
SendBigFishMessageRequest,
} from '../../../packages/shared/src/contracts/bigFish';
import { parseApiErrorMessage } from '../../../packages/shared/src/http';
import type { TextStreamOptions } from '../aiTypes';
import {
type ApiRetryOptions,
fetchWithApiAuth,
requestJson,
} from '../apiClient';
import { readCreationAgentSessionFromSse } from '../creation-agent';
import { createCreationAgentClient } from '../creation-agent';
const BIG_FISH_AGENT_API_BASE = '/api/runtime/big-fish/agent/sessions';
const BIG_FISH_SESSION_START_TIMEOUT_MS = 15000;
const BIG_FISH_READ_RETRY: ApiRetryOptions = {
maxRetries: 1,
baseDelayMs: 180,
maxDelayMs: 480,
};
const BIG_FISH_WRITE_RETRY: ApiRetryOptions = {
maxRetries: 1,
baseDelayMs: 240,
maxDelayMs: 640,
retryUnsafeMethods: true,
};
const bigFishAgentHttpClient = createCreationAgentClient<
CreateBigFishSessionRequest,
BigFishSessionResponse,
BigFishSessionResponse,
BigFishSessionSnapshotResponse,
SendBigFishMessageRequest,
BigFishSessionResponse,
ExecuteBigFishActionRequest,
BigFishActionResponse
>({
apiBase: BIG_FISH_AGENT_API_BASE,
messages: {
createSession: '创建大鱼吃小鱼共创会话失败',
getSession: '读取大鱼吃小鱼共创会话失败',
sendMessage: '发送大鱼吃小鱼共创消息失败',
streamIncomplete: '大鱼吃小鱼共创消息流式结果不完整',
executeAction: '执行大鱼吃小鱼共创操作失败',
},
});
export async function createBigFishCreationSession(
payload: CreateBigFishSessionRequest = {},
) {
return requestJson<BigFishSessionResponse>(
BIG_FISH_AGENT_API_BASE,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
},
'创建大鱼吃小鱼共创会话失败',
{
retry: BIG_FISH_WRITE_RETRY,
timeoutMs: BIG_FISH_SESSION_START_TIMEOUT_MS,
},
);
return bigFishAgentHttpClient.createSession(payload);
}
export async function getBigFishCreationSession(sessionId: string) {
return requestJson<BigFishSessionResponse>(
`${BIG_FISH_AGENT_API_BASE}/${encodeURIComponent(sessionId)}`,
{
method: 'GET',
},
'读取大鱼吃小鱼共创会话失败',
{
retry: BIG_FISH_READ_RETRY,
},
);
return bigFishAgentHttpClient.getSession(sessionId);
}
export async function sendBigFishCreationMessage(
sessionId: string,
payload: SendBigFishMessageRequest,
) {
return requestJson<BigFishSessionResponse>(
`${BIG_FISH_AGENT_API_BASE}/${encodeURIComponent(sessionId)}/messages`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
},
'发送大鱼吃小鱼共创消息失败',
{
retry: BIG_FISH_WRITE_RETRY,
},
);
return bigFishAgentHttpClient.sendMessage(sessionId, payload);
}
export async function streamBigFishCreationMessage(
@@ -83,61 +52,14 @@ export async function streamBigFishCreationMessage(
payload: SendBigFishMessageRequest,
options: TextStreamOptions = {},
) {
const response = await openBigFishCreationSsePost(
`${BIG_FISH_AGENT_API_BASE}/${encodeURIComponent(sessionId)}/messages/stream`,
payload,
'发送大鱼吃小鱼共创消息失败',
);
return readCreationAgentSessionFromSse<BigFishSessionSnapshotResponse>(
response,
{
...options,
fallbackMessage: '发送大鱼吃小鱼共创消息失败',
incompleteMessage: '大鱼吃小鱼共创消息流式结果不完整',
},
);
return bigFishAgentHttpClient.streamMessage(sessionId, payload, options);
}
export async function executeBigFishCreationAction(
sessionId: string,
payload: ExecuteBigFishActionRequest,
) {
return requestJson<BigFishActionResponse>(
`${BIG_FISH_AGENT_API_BASE}/${encodeURIComponent(sessionId)}/actions`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
},
'执行大鱼吃小鱼共创操作失败',
{
retry: BIG_FISH_WRITE_RETRY,
},
);
}
async function openBigFishCreationSsePost(
url: string,
payload: unknown,
fallbackMessage: string,
) {
const response = await fetchWithApiAuth(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!response.ok) {
const responseText = await response.text();
throw new Error(parseApiErrorMessage(responseText, fallbackMessage));
}
if (!response.body) {
throw new Error('streaming response body is unavailable');
}
return response;
return bigFishAgentHttpClient.executeAction(sessionId, payload);
}
export const bigFishCreationClient = {

View File

@@ -0,0 +1,165 @@
import { parseApiErrorMessage } from '../../../packages/shared/src/http';
import type { TextStreamOptions } from '../aiTypes';
import {
type ApiRetryOptions,
fetchWithApiAuth,
requestJson,
} from '../apiClient';
import { readCreationAgentSessionFromSse } from './creationAgentSse';
type CreationAgentClientMessages = {
createSession: string;
getSession: string;
sendMessage: string;
streamIncomplete: string;
executeAction: string;
};
type CreationAgentClientOptions = {
apiBase: string;
messages: CreationAgentClientMessages;
createSessionTimeoutMs?: number;
readRetry?: ApiRetryOptions;
writeRetry?: ApiRetryOptions;
};
const DEFAULT_CREATION_AGENT_READ_RETRY: ApiRetryOptions = {
maxRetries: 1,
baseDelayMs: 180,
maxDelayMs: 480,
};
const DEFAULT_CREATION_AGENT_WRITE_RETRY: ApiRetryOptions = {
maxRetries: 1,
baseDelayMs: 240,
maxDelayMs: 640,
retryUnsafeMethods: true,
};
function buildJsonPostInit(payload: unknown): RequestInit {
return {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
};
}
async function openCreationAgentSsePost(
url: string,
payload: unknown,
fallbackMessage: string,
signal?: AbortSignal,
) {
const response = await fetchWithApiAuth(url, {
...buildJsonPostInit(payload),
signal,
});
if (!response.ok) {
const responseText = await response.text();
throw new Error(parseApiErrorMessage(responseText, fallbackMessage));
}
if (!response.body) {
throw new Error('streaming response body is unavailable');
}
return response;
}
/**
* 三类作品创作 Agent 都遵循同一组 HTTP/SSE 端点形状。
* 这里统一请求骨架,玩法 client 只保留路径、类型与中文错误文案差异。
*/
export function createCreationAgentClient<
TCreateSessionPayload,
TCreateSessionResponse,
TGetSessionResponse,
TSession,
TSendMessagePayload,
TSendMessageResponse,
TExecuteActionPayload,
TExecuteActionResponse,
>({
apiBase,
messages,
createSessionTimeoutMs = 15000,
readRetry = DEFAULT_CREATION_AGENT_READ_RETRY,
writeRetry = DEFAULT_CREATION_AGENT_WRITE_RETRY,
}: CreationAgentClientOptions) {
const createSession = (
payload: TCreateSessionPayload,
): Promise<TCreateSessionResponse> =>
requestJson<TCreateSessionResponse>(
apiBase,
buildJsonPostInit(payload),
messages.createSession,
{
retry: writeRetry,
timeoutMs: createSessionTimeoutMs,
},
);
const getSession = (sessionId: string): Promise<TGetSessionResponse> =>
requestJson<TGetSessionResponse>(
`${apiBase}/${encodeURIComponent(sessionId)}`,
{ method: 'GET' },
messages.getSession,
{
retry: readRetry,
},
);
const sendMessage = (
sessionId: string,
payload: TSendMessagePayload,
): Promise<TSendMessageResponse> =>
requestJson<TSendMessageResponse>(
`${apiBase}/${encodeURIComponent(sessionId)}/messages`,
buildJsonPostInit(payload),
messages.sendMessage,
{
retry: writeRetry,
},
);
const streamMessage = async (
sessionId: string,
payload: TSendMessagePayload,
options: TextStreamOptions = {},
): Promise<TSession> => {
const response = await openCreationAgentSsePost(
`${apiBase}/${encodeURIComponent(sessionId)}/messages/stream`,
payload,
messages.sendMessage,
options.signal,
);
return readCreationAgentSessionFromSse<TSession>(response, {
...options,
fallbackMessage: messages.sendMessage,
incompleteMessage: messages.streamIncomplete,
});
};
const executeAction = (
sessionId: string,
payload: TExecuteActionPayload,
): Promise<TExecuteActionResponse> =>
requestJson<TExecuteActionResponse>(
`${apiBase}/${encodeURIComponent(sessionId)}/actions`,
buildJsonPostInit(payload),
messages.executeAction,
{
retry: writeRetry,
},
);
return {
createSession,
getSession,
sendMessage,
streamMessage,
executeAction,
};
}

View File

@@ -0,0 +1,51 @@
/* @vitest-environment jsdom */
import { afterEach, expect, test, vi } from 'vitest';
import {
parseCreationAgentDocumentInput,
validateCreationAgentDocumentInputFile,
} from './creationAgentDocumentInput';
afterEach(() => {
vi.unstubAllGlobals();
});
test('creation agent document input validation accepts supported text documents', () => {
expect(() => {
validateCreationAgentDocumentInputFile(
new File(['世界设定'], '世界设定.MD', { type: 'text/markdown' }),
);
}).not.toThrow();
});
test('creation agent document input validation rejects unsupported documents', () => {
expect(() => {
validateCreationAgentDocumentInputFile(
new File(['binary'], '世界设定.docx', {
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
}),
);
}).toThrow('暂时只支持 txt、md、csv、json 文本文档。');
});
test('creation agent document input validation rejects oversized documents', () => {
const oversizedContent = new Uint8Array(256 * 1024 + 1);
expect(() => {
validateCreationAgentDocumentInputFile(
new File([oversizedContent], '世界设定.txt', { type: 'text/plain' }),
);
}).toThrow('文档过大,请上传 256KB 以内的文本文件。');
});
test('creation agent document input parse skips network for unsupported files', async () => {
const fetchSpy = vi.fn();
vi.stubGlobal('fetch', fetchSpy);
await expect(
parseCreationAgentDocumentInput(new File(['binary'], '世界设定.docx')),
).rejects.toThrow('暂时只支持 txt、md、csv、json 文本文档。');
expect(fetchSpy).not.toHaveBeenCalled();
});

View File

@@ -0,0 +1,82 @@
import type {
ParseCreationAgentDocumentInputRequest,
ParseCreationAgentDocumentInputResponse,
} from '../../../packages/shared/src/contracts/creationAgentDocumentInput';
import { requestJson } from '../apiClient';
const DOCUMENT_INPUT_PARSE_ENDPOINT =
'/api/runtime/creation-agent/document-inputs/parse';
const MAX_DOCUMENT_INPUT_BYTES = 256 * 1024;
const SUPPORTED_DOCUMENT_INPUT_EXTENSIONS = new Set([
'txt',
'md',
'markdown',
'csv',
'json',
]);
export async function parseCreationAgentDocumentInput(
file: File,
): Promise<ParseCreationAgentDocumentInputResponse> {
validateCreationAgentDocumentInputFile(file);
const contentBase64 = await readFileAsBase64(file);
const payload: ParseCreationAgentDocumentInputRequest = {
fileName: file.name,
contentType: file.type || null,
contentBase64,
};
return requestJson<ParseCreationAgentDocumentInputResponse>(
DOCUMENT_INPUT_PARSE_ENDPOINT,
{
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify(payload),
},
'解析文档失败',
{
retry: {
maxRetries: 0,
},
},
);
}
export function validateCreationAgentDocumentInputFile(file: File) {
const fileName = file.name.trim();
const extension = fileName.includes('.')
? fileName.split('.').pop()?.trim().toLowerCase()
: '';
if (!extension || !SUPPORTED_DOCUMENT_INPUT_EXTENSIONS.has(extension)) {
throw new Error('暂时只支持 txt、md、csv、json 文本文档。');
}
if (file.size <= 0) {
throw new Error('文档内容为空,请选择有内容的文件。');
}
if (file.size > MAX_DOCUMENT_INPUT_BYTES) {
throw new Error('文档过大,请上传 256KB 以内的文本文件。');
}
}
function readFileAsBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = () => {
reject(new Error('读取文档失败,请重新选择文件。'));
};
reader.onload = () => {
const result = typeof reader.result === 'string' ? reader.result : '';
const commaIndex = result.indexOf(',');
resolve(commaIndex >= 0 ? result.slice(commaIndex + 1) : result);
};
reader.readAsDataURL(file);
});
}

View File

@@ -1,3 +1,5 @@
export * from './creationAgentClientFactory';
export * from './creationAgentChat';
export * from './creationAgentDocumentInput';
export * from './creationAgentProgress';
export * from './creationAgentSse';

View File

@@ -185,11 +185,30 @@ test('keeps failed draft foundation progress on explicit failure state instead o
expect(progress?.phaseId).toBe('failed');
expect(progress?.phaseLabel).toBe('底稿生成失败');
expect(progress?.phaseDetail).toContain('角色主形象补齐失败');
expect(progress?.overallProgress).toBeLessThan(100);
expect(progress?.estimatedRemainingMs).toBeNull();
expect(progress?.steps.some((step) => step.label === '编译草稿卡')).toBe(true);
expect(progress?.steps.some((step) => step.status === 'active')).toBe(false);
expect(progress?.steps.filter((step) => step.status === 'completed').length).toBeGreaterThan(0);
});
test('estimates draft generation wait time from phase duration model instead of linear progress', () => {
const progress = buildAgentDraftFoundationGenerationProgress(
{
...baseOperation,
phaseLabel: '生成幕背景图',
phaseDetail: '正在生成幕背景图 1/6潮汐码头。',
progress: 98,
updatedAt: '1970-01-01T00:00:01.000Z',
},
1_000,
6_000,
);
expect(progress?.estimatedRemainingMs).toBeGreaterThan(80_000);
expect(progress?.estimatedRemainingMs).toBeLessThan(140_000);
});
test('builds readable draft setting text from creator intent first', () => {
const settingText = buildAgentDraftFoundationSettingText(baseSession);

View File

@@ -199,6 +199,7 @@ type AgentDraftFoundationStepDefinition = {
detail: string;
matchers: string[];
minProgress: number;
expectedDurationMs: number;
};
type AgentDraftFoundationFailedStep = {
@@ -215,6 +216,7 @@ const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [
detail: '正在校验当前锚点并准备底稿编译链路。',
matchers: ['已接收请求'],
minProgress: 0,
expectedDurationMs: 3_000,
},
{
id: 'framework',
@@ -222,6 +224,7 @@ const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [
detail: '正在生成第一版世界框架、主题与核心冲突。',
matchers: ['整理世界骨架', '生成世界底稿'],
minProgress: 12,
expectedDurationMs: 25_000,
},
{
id: 'playable-outline',
@@ -229,6 +232,7 @@ const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [
detail: '正在补出玩家视角角色的首轮名单与定位。',
matchers: ['生成可扮演角色'],
minProgress: 16,
expectedDurationMs: 18_000,
},
{
id: 'story-outline',
@@ -236,6 +240,7 @@ const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [
detail: '正在整理关键 NPC、势力接口人与关系入口。',
matchers: ['生成场景角色'],
minProgress: 30,
expectedDurationMs: 45_000,
},
{
id: 'landmark-seed',
@@ -243,6 +248,7 @@ const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [
detail: '正在补出第一批关键场景与地点骨架。',
matchers: ['生成关键场景'],
minProgress: 44,
expectedDurationMs: 18_000,
},
{
id: 'landmark-network',
@@ -250,6 +256,7 @@ const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [
detail: '正在串联地点关系、线程挂钩与角色分布。',
matchers: ['建立场景连接'],
minProgress: 56,
expectedDurationMs: 18_000,
},
{
id: 'playable-detail',
@@ -257,6 +264,7 @@ const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [
detail: '正在补全可扮演角色的叙事基础与档案细节。',
matchers: ['补全可扮演角色'],
minProgress: 66,
expectedDurationMs: 32_000,
},
{
id: 'story-detail',
@@ -264,6 +272,7 @@ const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [
detail: '正在补全场景角色的叙事基础与档案细节。',
matchers: ['补全场景角色'],
minProgress: 84,
expectedDurationMs: 65_000,
},
{
id: 'finalize',
@@ -271,6 +280,7 @@ const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [
detail: '正在把分批生成结果汇总成第一版可浏览的世界底稿。',
matchers: ['编译世界底稿'],
minProgress: 97,
expectedDurationMs: 6_000,
},
{
id: 'role-visuals',
@@ -278,6 +288,7 @@ const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [
detail: '正在为关键角色补主形象预览资源。',
matchers: ['生成角色主形象'],
minProgress: 97,
expectedDurationMs: 85_000,
},
{
id: 'act-backgrounds',
@@ -285,6 +296,7 @@ const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [
detail: '正在为场景章节的每一幕补背景图预览资源。',
matchers: ['生成幕背景图'],
minProgress: 98,
expectedDurationMs: 85_000,
},
{
id: 'cards',
@@ -292,6 +304,7 @@ const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [
detail: '正在整理世界卡、角色卡、地点卡与详情结构。',
matchers: ['编译草稿卡'],
minProgress: 99,
expectedDurationMs: 15_000,
},
{
id: 'workspace',
@@ -299,6 +312,7 @@ const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [
detail: '正在写回草稿数据,并打开可继续完善的结果页。',
matchers: ['世界底稿已生成'],
minProgress: 100,
expectedDurationMs: 4_000,
},
] as const satisfies ReadonlyArray<AgentDraftFoundationStepDefinition>;
@@ -333,6 +347,39 @@ function resolveAgentDraftFoundationStepIndexByProgress(progress: number) {
return matchedIndex;
}
function resolveFailedProgress(
operation: CustomWorldAgentOperationRecord,
activeStepIndex: number,
) {
const progress = clampProgress(operation.progress);
if (operation.status !== 'failed') {
return progress;
}
if (progress < 100) {
return progress;
}
const activeStep =
AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS[activeStepIndex] ??
AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS[0];
return Math.max(0, Math.min(99, activeStep.minProgress));
}
function parseOperationUpdatedAtMs(
operation: CustomWorldAgentOperationRecord,
) {
const rawUpdatedAt = operation.updatedAt?.trim();
if (!rawUpdatedAt) {
return null;
}
const parsedMs = Date.parse(rawUpdatedAt);
return Number.isFinite(parsedMs) ? parsedMs : null;
}
function resolveAgentDraftFoundationStepIndex(
operation: CustomWorldAgentOperationRecord,
) {
@@ -410,19 +457,47 @@ function resolveEstimatedRemainingMs(
startedAtMs: number | null,
nowMs: number,
status: CustomWorldAgentOperationRecord['status'],
activeStepIndex: number,
operationUpdatedAtMs: number | null,
) {
if (status === 'completed') {
return 0;
}
if (!startedAtMs || progress <= 0 || progress >= 100) {
if (status === 'failed' || progress >= 100) {
return null;
}
const elapsedMs = Math.max(0, nowMs - startedAtMs);
const progressFraction = progress / 100;
const activeStep =
AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS[activeStepIndex] ??
AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS[0];
const nextStep =
AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS[activeStepIndex + 1] ??
activeStep;
const phaseProgressRange = Math.max(
1,
nextStep.minProgress - activeStep.minProgress,
);
const phaseProgressRatio = Math.max(
0,
Math.min(0.95, (progress - activeStep.minProgress) / phaseProgressRange),
);
const phaseStartedAtMs = operationUpdatedAtMs ?? startedAtMs;
const currentPhaseElapsedMs = phaseStartedAtMs
? Math.max(0, nowMs - phaseStartedAtMs)
: 0;
const currentPhaseRemainingMs = Math.max(
0,
Math.round(
activeStep.expectedDurationMs * (1 - phaseProgressRatio) -
currentPhaseElapsedMs,
),
);
const followingStepsRemainingMs = AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS.slice(
activeStepIndex + 1,
).reduce((sum, step) => sum + step.expectedDurationMs, 0);
return Math.max(0, Math.round(elapsedMs / progressFraction - elapsedMs));
return currentPhaseRemainingMs + followingStepsRemainingMs;
}
export function isDraftFoundationOperation(
@@ -449,14 +524,16 @@ export function buildAgentDraftFoundationGenerationProgress(
return null;
}
const overallProgress = clampProgress(operation.progress);
const activeStepIndex = resolveAgentDraftFoundationStepIndex(operation);
const overallProgress = resolveFailedProgress(operation, activeStepIndex);
const elapsedMs = startedAtMs ? Math.max(0, nowMs - startedAtMs) : 0;
const estimatedRemainingMs = resolveEstimatedRemainingMs(
overallProgress,
startedAtMs,
nowMs,
operation.status,
activeStepIndex,
parseOperationUpdatedAtMs(operation),
);
const failedStep = resolveAgentDraftFoundationFailedStep(operation);
const activeStep =

View File

@@ -0,0 +1,350 @@
import type {
EightAnchorContent,
KeyRelationshipValue,
} from '../../packages/shared/src/contracts/customWorldAgent';
import { normalizeCustomWorldCreatorIntent } from './customWorldCreatorIntent';
import type { CustomWorldProfile } from '../types';
export type CustomWorldFoundationEntryId =
| 'world-promise'
| 'player-fantasy'
| 'theme-boundary'
| 'player-entry-point'
| 'core-conflict'
| 'key-relationships'
| 'hidden-lines'
| 'iconic-elements';
export type CustomWorldFoundationEntry = {
id: CustomWorldFoundationEntryId;
label: string;
value: string;
};
export function compactFoundationTextList(
values: Array<string | null | undefined>,
) {
return values.map((value) => value?.trim()).filter(Boolean) as string[];
}
export function parseFoundationTagText(value: string) {
return value
.split(/[;]/u)
.map((item) => item.trim())
.filter(Boolean);
}
function toText(value: unknown) {
return typeof value === 'string' ? value.trim() : '';
}
function toTextArray(value: unknown) {
return Array.isArray(value)
? value.map((item) => toText(item)).filter(Boolean)
: [];
}
function toRecord(value: unknown) {
return value && typeof value === 'object' && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
function buildRelationshipSeedText(value: unknown) {
const record = toRecord(value);
if (!record) {
return '';
}
return compactFoundationTextList([
toText(record.name),
toText(record.role),
toText(record.relationToPlayer)
? `与玩家:${toText(record.relationToPlayer)}`
: '',
toText(record.hiddenHook) ? `代价/暗线:${toText(record.hiddenHook)}` : '',
]).join('');
}
function buildKeyRelationshipText(value: KeyRelationshipValue) {
return compactFoundationTextList([
value.pairs,
value.relationshipType,
value.secretOrCost ? `代价/秘密:${value.secretOrCost}` : '',
]).join('');
}
function buildAnchorContentFromProfileFallback(
profile: CustomWorldProfile,
): EightAnchorContent {
const creatorIntent = normalizeCustomWorldCreatorIntent(profile.creatorIntent);
const relationshipSeed = creatorIntent?.keyCharacters[0] ?? null;
return {
worldPromise: {
hook:
creatorIntent?.worldHook ||
profile.anchorPack?.worldSummary ||
profile.summary,
differentiator: profile.subtitle || profile.settingText,
desiredExperience:
compactFoundationTextList([
creatorIntent?.toneDirectives.join('、') || '',
profile.tone,
]).join('') || profile.tone,
},
playerFantasy: {
playerRole: creatorIntent?.playerPremise || profile.playerGoal,
corePursuit: profile.playerGoal,
fearOfLoss:
relationshipSeed?.hiddenHook ||
creatorIntent?.coreConflicts[0] ||
profile.coreConflicts[0] ||
'',
},
themeBoundary: {
toneKeywords: compactFoundationTextList([
creatorIntent?.themeKeywords.join('、') || '',
creatorIntent?.toneDirectives.join('、') || '',
]),
aestheticDirectives: compactFoundationTextList([
profile.tone,
profile.subtitle,
]),
forbiddenDirectives: creatorIntent?.forbiddenDirectives ?? [],
},
playerEntryPoint: {
openingIdentity: creatorIntent?.playerPremise || '',
openingProblem:
creatorIntent?.openingSituation || profile.coreConflicts[0] || '',
entryMotivation: profile.playerGoal,
},
coreConflict: {
surfaceConflicts:
creatorIntent?.coreConflicts.length
? creatorIntent.coreConflicts
: profile.coreConflicts,
hiddenCrisis:
relationshipSeed?.hiddenHook ||
profile.summary ||
profile.settingText,
firstTouchedConflict:
creatorIntent?.openingSituation ||
profile.coreConflicts[0] ||
profile.playerGoal,
},
keyRelationships: relationshipSeed
? [
{
pairs: compactFoundationTextList([
relationshipSeed.name,
relationshipSeed.role,
]).join(' · '),
relationshipType: relationshipSeed.relationToPlayer || '',
secretOrCost: relationshipSeed.hiddenHook || '',
},
]
: [],
hiddenLines: {
hiddenTruths: compactFoundationTextList([
relationshipSeed?.hiddenHook || '',
profile.summary,
]),
misdirectionHints: compactFoundationTextList([
profile.subtitle,
profile.majorFactions[0] || '',
]),
revealPacing:
creatorIntent?.openingSituation ||
profile.coreConflicts[0] ||
profile.playerGoal,
},
iconicElements: {
iconicMotifs:
creatorIntent?.iconicElements.length
? creatorIntent.iconicElements
: compactFoundationTextList([
profile.anchorPack?.motifDirectives.join('、') || '',
profile.landmarks[0]?.name || '',
]),
institutionsOrArtifacts: compactFoundationTextList([
profile.camp?.name || '',
profile.majorFactions[0] || '',
]),
hardRules: compactFoundationTextList([
profile.playerGoal,
profile.coreConflicts[0] || '',
]),
},
} satisfies EightAnchorContent;
}
export function getCustomWorldFoundationAnchorContent(
profile: CustomWorldProfile,
) {
const anchorContentRecord = profile.anchorContent;
if (!anchorContentRecord) {
return buildAnchorContentFromProfileFallback(profile);
}
const worldPromiseRecord = toRecord(anchorContentRecord.worldPromise);
const playerFantasyRecord = toRecord(anchorContentRecord.playerFantasy);
const themeBoundaryRecord = toRecord(anchorContentRecord.themeBoundary);
const playerEntryPointRecord = toRecord(anchorContentRecord.playerEntryPoint);
const coreConflictRecord = toRecord(anchorContentRecord.coreConflict);
const hiddenLinesRecord = toRecord(anchorContentRecord.hiddenLines);
const iconicElementsRecord = toRecord(anchorContentRecord.iconicElements);
return {
worldPromise: worldPromiseRecord
? {
hook: toText(worldPromiseRecord.hook),
differentiator: toText(worldPromiseRecord.differentiator),
desiredExperience: toText(worldPromiseRecord.desiredExperience),
}
: null,
playerFantasy: playerFantasyRecord
? {
playerRole: toText(playerFantasyRecord.playerRole),
corePursuit: toText(playerFantasyRecord.corePursuit),
fearOfLoss: toText(playerFantasyRecord.fearOfLoss),
}
: null,
themeBoundary: themeBoundaryRecord
? {
toneKeywords: toTextArray(themeBoundaryRecord.toneKeywords),
aestheticDirectives: toTextArray(
themeBoundaryRecord.aestheticDirectives,
),
forbiddenDirectives: toTextArray(themeBoundaryRecord.forbiddenDirectives),
}
: null,
playerEntryPoint: playerEntryPointRecord
? {
openingIdentity: toText(playerEntryPointRecord.openingIdentity),
openingProblem: toText(playerEntryPointRecord.openingProblem),
entryMotivation: toText(playerEntryPointRecord.entryMotivation),
}
: null,
coreConflict: coreConflictRecord
? {
surfaceConflicts: toTextArray(coreConflictRecord.surfaceConflicts),
hiddenCrisis: toText(coreConflictRecord.hiddenCrisis),
firstTouchedConflict: toText(coreConflictRecord.firstTouchedConflict),
}
: null,
keyRelationships: Array.isArray(anchorContentRecord.keyRelationships)
? anchorContentRecord.keyRelationships
.map((entry) => toRecord(entry))
.filter(Boolean)
.map((entry) => ({
pairs: toText(entry?.pairs),
relationshipType: toText(entry?.relationshipType),
secretOrCost: toText(entry?.secretOrCost),
}))
: [],
hiddenLines: hiddenLinesRecord
? {
hiddenTruths: toTextArray(hiddenLinesRecord.hiddenTruths),
misdirectionHints: toTextArray(hiddenLinesRecord.misdirectionHints),
revealPacing: toText(hiddenLinesRecord.revealPacing),
}
: null,
iconicElements: iconicElementsRecord
? {
iconicMotifs: toTextArray(iconicElementsRecord.iconicMotifs),
institutionsOrArtifacts: toTextArray(
iconicElementsRecord.institutionsOrArtifacts,
),
hardRules: toTextArray(iconicElementsRecord.hardRules),
}
: null,
} satisfies EightAnchorContent;
}
export function buildCustomWorldFoundationEntries(
profile: CustomWorldProfile,
): CustomWorldFoundationEntry[] {
const creatorIntent = normalizeCustomWorldCreatorIntent(profile.creatorIntent);
const anchorContent = getCustomWorldFoundationAnchorContent(profile);
const fallbackRelationshipText =
buildRelationshipSeedText(creatorIntent?.keyCharacters[0]) ||
profile.playableNpcs[0]?.relationshipHooks.join('') ||
profile.storyNpcs[0]?.relationshipHooks.join('') ||
'';
return [
{
id: 'world-promise',
label: '世界承诺',
value: compactFoundationTextList([
anchorContent.worldPromise?.hook || '',
anchorContent.worldPromise?.differentiator || '',
anchorContent.worldPromise?.desiredExperience || '',
]).join(''),
},
{
id: 'player-fantasy',
label: '玩家幻想',
value: compactFoundationTextList([
anchorContent.playerFantasy?.playerRole || '',
anchorContent.playerFantasy?.corePursuit || '',
anchorContent.playerFantasy?.fearOfLoss || '',
]).join(''),
},
{
id: 'theme-boundary',
label: '主题边界',
value: compactFoundationTextList([
anchorContent.themeBoundary?.toneKeywords.join('、') || '',
anchorContent.themeBoundary?.aestheticDirectives.join('、') || '',
anchorContent.themeBoundary?.forbiddenDirectives.length
? `避免:${anchorContent.themeBoundary.forbiddenDirectives.join('、')}`
: '',
]).join(''),
},
{
id: 'player-entry-point',
label: '玩家切入口',
value: compactFoundationTextList([
anchorContent.playerEntryPoint?.openingIdentity || '',
anchorContent.playerEntryPoint?.openingProblem || '',
anchorContent.playerEntryPoint?.entryMotivation || '',
]).join(''),
},
{
id: 'core-conflict',
label: '核心冲突',
value: compactFoundationTextList([
anchorContent.coreConflict?.surfaceConflicts.join('、') || '',
anchorContent.coreConflict?.hiddenCrisis || '',
anchorContent.coreConflict?.firstTouchedConflict || '',
]).join(''),
},
{
id: 'key-relationships',
label: '关键关系',
value:
anchorContent.keyRelationships.map(buildKeyRelationshipText).join('\n') ||
fallbackRelationshipText,
},
{
id: 'hidden-lines',
label: '暗线与揭示',
value: compactFoundationTextList([
anchorContent.hiddenLines?.hiddenTruths.join('、') || '',
anchorContent.hiddenLines?.misdirectionHints.join('、') || '',
anchorContent.hiddenLines?.revealPacing || '',
]).join(''),
},
{
id: 'iconic-elements',
label: '标志元素',
value: compactFoundationTextList([
anchorContent.iconicElements?.iconicMotifs.join('、') || '',
anchorContent.iconicElements?.institutionsOrArtifacts.join('、') || '',
anchorContent.iconicElements?.hardRules.join('、') || '',
]).join(''),
},
];
}

View File

@@ -8,28 +8,29 @@ import type {
PuzzleAgentSessionSnapshot,
SendPuzzleAgentMessageRequest,
} from '../../../packages/shared/src/contracts/puzzleAgentSession';
import { parseApiErrorMessage } from '../../../packages/shared/src/http';
import type { TextStreamOptions } from '../aiTypes';
import {
type ApiRetryOptions,
fetchWithApiAuth,
requestJson,
} from '../apiClient';
import { readCreationAgentSessionFromSse } from '../creation-agent';
import { createCreationAgentClient } from '../creation-agent';
const PUZZLE_AGENT_API_BASE = '/api/runtime/puzzle/agent/sessions';
const PUZZLE_AGENT_SESSION_START_TIMEOUT_MS = 15000;
const PUZZLE_AGENT_READ_RETRY: ApiRetryOptions = {
maxRetries: 1,
baseDelayMs: 180,
maxDelayMs: 480,
};
const PUZZLE_AGENT_WRITE_RETRY: ApiRetryOptions = {
maxRetries: 1,
baseDelayMs: 240,
maxDelayMs: 640,
retryUnsafeMethods: true,
};
const puzzleAgentHttpClient = createCreationAgentClient<
CreatePuzzleAgentSessionRequest,
CreatePuzzleAgentSessionResponse,
CreatePuzzleAgentSessionResponse,
PuzzleAgentSessionSnapshot,
SendPuzzleAgentMessageRequest,
{ session: PuzzleAgentSessionSnapshot },
PuzzleAgentActionRequest,
PuzzleAgentActionResponse
>({
apiBase: PUZZLE_AGENT_API_BASE,
messages: {
createSession: '创建拼图共创会话失败',
getSession: '读取拼图共创会话失败',
sendMessage: '发送拼图共创消息失败',
streamIncomplete: '拼图共创消息流式结果不完整',
executeAction: '执行拼图共创操作失败',
},
});
/**
* 创建拼图 Agent 共创会话。
@@ -38,35 +39,14 @@ const PUZZLE_AGENT_WRITE_RETRY: ApiRetryOptions = {
export async function createPuzzleAgentSession(
payload: CreatePuzzleAgentSessionRequest = {},
) {
return requestJson<CreatePuzzleAgentSessionResponse>(
PUZZLE_AGENT_API_BASE,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
},
'创建拼图共创会话失败',
{
retry: PUZZLE_AGENT_WRITE_RETRY,
timeoutMs: PUZZLE_AGENT_SESSION_START_TIMEOUT_MS,
},
);
return puzzleAgentHttpClient.createSession(payload);
}
/**
* 读取拼图 Agent 会话快照。
*/
export async function getPuzzleAgentSession(sessionId: string) {
return requestJson<CreatePuzzleAgentSessionResponse>(
`${PUZZLE_AGENT_API_BASE}/${encodeURIComponent(sessionId)}`,
{
method: 'GET',
},
'读取拼图共创会话失败',
{
retry: PUZZLE_AGENT_READ_RETRY,
},
);
return puzzleAgentHttpClient.getSession(sessionId);
}
/**
@@ -77,18 +57,7 @@ export async function sendPuzzleAgentMessage(
sessionId: string,
payload: SendPuzzleAgentMessageRequest,
) {
return requestJson<{ session: PuzzleAgentSessionSnapshot }>(
`${PUZZLE_AGENT_API_BASE}/${encodeURIComponent(sessionId)}/messages`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
},
'发送拼图共创消息失败',
{
retry: PUZZLE_AGENT_WRITE_RETRY,
},
);
return puzzleAgentHttpClient.sendMessage(sessionId, payload);
}
/**
@@ -100,20 +69,7 @@ export async function streamPuzzleAgentMessage(
payload: SendPuzzleAgentMessageRequest,
options: TextStreamOptions = {},
) {
const response = await openPuzzleAgentSsePost(
`${PUZZLE_AGENT_API_BASE}/${encodeURIComponent(sessionId)}/messages/stream`,
payload,
'发送拼图共创消息失败',
);
return readCreationAgentSessionFromSse<PuzzleAgentSessionSnapshot>(
response,
{
...options,
fallbackMessage: '发送拼图共创消息失败',
incompleteMessage: '拼图共创消息流式结果不完整',
},
);
return puzzleAgentHttpClient.streamMessage(sessionId, payload, options);
}
/**
@@ -124,41 +80,7 @@ export async function executePuzzleAgentAction(
sessionId: string,
payload: PuzzleAgentActionRequest,
) {
return requestJson<PuzzleAgentActionResponse>(
`${PUZZLE_AGENT_API_BASE}/${encodeURIComponent(sessionId)}/actions`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
},
'执行拼图共创操作失败',
{
retry: PUZZLE_AGENT_WRITE_RETRY,
},
);
}
async function openPuzzleAgentSsePost(
url: string,
payload: unknown,
fallbackMessage: string,
) {
const response = await fetchWithApiAuth(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!response.ok) {
const responseText = await response.text();
throw new Error(parseApiErrorMessage(responseText, fallbackMessage));
}
if (!response.body) {
throw new Error('streaming response body is unavailable');
}
return response;
return puzzleAgentHttpClient.executeAction(sessionId, payload);
}
export const puzzleAgentClient = {

View File

@@ -11,8 +11,10 @@ export {
} from './rpgEntryLibraryClient';
export {
clearRpgProfileBrowseHistory,
createRpgProfileRechargeOrder,
getRpgProfileDashboard,
getRpgProfilePlayStats,
getRpgProfileRechargeCenter,
getRpgProfileSettings,
getRpgProfileWalletLedger,
listRpgProfileBrowseHistory,

View File

@@ -1,9 +1,11 @@
import type {
CreateProfileRechargeOrderResponse,
PlatformBrowseHistoryBatchSyncRequest,
PlatformBrowseHistoryResponse,
PlatformBrowseHistoryWriteEntry,
ProfileDashboardSummary,
ProfilePlayStatsResponse,
ProfileRechargeCenterResponse,
ProfileSaveArchiveListResponse,
ProfileSaveArchiveResumeResponse,
ProfileWalletLedgerResponse,
@@ -67,6 +69,33 @@ export function getRpgProfileWalletLedger(
);
}
export function getRpgProfileRechargeCenter(
options: RuntimeRequestOptions = {},
) {
return requestRpgRuntimeJson<ProfileRechargeCenterResponse>(
'/profile/recharge-center',
{ method: 'GET' },
'读取账户充值失败',
options,
);
}
export function createRpgProfileRechargeOrder(
productId: string,
options: RuntimeRequestOptions = {},
) {
return requestRpgRuntimeJson<CreateProfileRechargeOrderResponse>(
'/profile/recharge/orders',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ productId, paymentChannel: 'mock' }),
},
'充值失败',
options,
);
}
export function getRpgProfilePlayStats(options: RuntimeRequestOptions = {}) {
return requestRpgRuntimeJson<ProfilePlayStatsResponse>(
'/profile/play-stats',
@@ -176,6 +205,8 @@ export const rpgProfileClient = {
getDashboard: getRpgProfileDashboard,
getPlayStats: getRpgProfilePlayStats,
getWalletLedger: getRpgProfileWalletLedger,
getRechargeCenter: getRpgProfileRechargeCenter,
createRechargeOrder: createRpgProfileRechargeOrder,
getSettings: getRpgProfileSettings,
putSettings: putRpgProfileSettings,
listSaveArchives: listRpgProfileSaveArchives,

View File

@@ -0,0 +1,23 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
import {
RuntimeProfileRechargeOrderCreateInput,
RuntimeProfileRechargeCenterProcedureResult,
} from "./types";
export const params = {
get input() {
return RuntimeProfileRechargeOrderCreateInput;
},
};
export const returnType = RuntimeProfileRechargeCenterProcedureResult

View File

@@ -17,6 +17,8 @@ import {
export default __t.row({
profileId: __t.string().primaryKey().name("profile_id"),
ownerUserId: __t.string().name("owner_user_id"),
publicWorkCode: __t.string().name("public_work_code"),
authorPublicUserCode: __t.string().name("author_public_user_code"),
authorDisplayName: __t.string().name("author_display_name"),
worldName: __t.string().name("world_name"),
subtitle: __t.string(),

View File

@@ -0,0 +1,23 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
import {
BigFishWorkDeleteInput,
BigFishWorksProcedureResult,
} from "./types";
export const params = {
get input() {
return BigFishWorkDeleteInput;
},
};
export const returnType = BigFishWorksProcedureResult

View File

@@ -0,0 +1,23 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
import {
CustomWorldAgentSessionGetInput,
CustomWorldWorksListResult,
} from "./types";
export const params = {
get input() {
return CustomWorldAgentSessionGetInput;
},
};
export const returnType = CustomWorldWorksListResult

View File

@@ -0,0 +1,23 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
import {
PuzzleWorkDeleteInput,
PuzzleWorksProcedureResult,
} from "./types";
export const params = {
get input() {
return PuzzleWorkDeleteInput;
},
};
export const returnType = PuzzleWorksProcedureResult

View File

@@ -0,0 +1,19 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
import {
AuthStoreSnapshotProcedureResult,
} from "./types";
export const params = {
};
export const returnType = AuthStoreSnapshotProcedureResult

View File

@@ -0,0 +1,23 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
import {
BigFishSessionProcedureResult,
BigFishMessageFinalizeInput,
} from "./types";
export const params = {
get input() {
return BigFishMessageFinalizeInput;
},
};
export const returnType = BigFishSessionProcedureResult

View File

@@ -0,0 +1,23 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
import {
PuzzleAgentSessionProcedureResult,
PuzzleAgentMessageFinalizeInput,
} from "./types";
export const params = {
get input() {
return PuzzleAgentMessageFinalizeInput;
},
};
export const returnType = PuzzleAgentSessionProcedureResult

View File

@@ -0,0 +1,19 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
import {
AuthStoreSnapshotProcedureResult,
} from "./types";
export const params = {
};
export const returnType = AuthStoreSnapshotProcedureResult

View File

@@ -0,0 +1,23 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
import {
CustomWorldLibraryMutationResult,
CustomWorldGalleryDetailByCodeInput,
} from "./types";
export const params = {
get input() {
return CustomWorldGalleryDetailByCodeInput;
},
};
export const returnType = CustomWorldLibraryMutationResult

View File

@@ -0,0 +1,23 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
import {
RuntimeProfileRechargeCenterProcedureResult,
RuntimeProfileRechargeCenterGetInput,
} from "./types";
export const params = {
get input() {
return RuntimeProfileRechargeCenterGetInput;
},
};
export const returnType = RuntimeProfileRechargeCenterProcedureResult

View File

@@ -0,0 +1,19 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
import {
AuthStoreSnapshotImportProcedureResult,
} from "./types";
export const params = {
};
export const returnType = AuthStoreSnapshotImportProcedureResult

View File

@@ -79,14 +79,22 @@ import * as CreateAiTaskAndReturnProcedure from "./create_ai_task_and_return_pro
import * as CreateBattleStateAndReturnProcedure from "./create_battle_state_and_return_procedure";
import * as CreateBigFishSessionProcedure from "./create_big_fish_session_procedure";
import * as CreateCustomWorldAgentSessionProcedure from "./create_custom_world_agent_session_procedure";
import * as CreateProfileRechargeOrderAndReturnProcedure from "./create_profile_recharge_order_and_return_procedure";
import * as CreatePuzzleAgentSessionProcedure from "./create_puzzle_agent_session_procedure";
import * as DeleteBigFishWorkProcedure from "./delete_big_fish_work_procedure";
import * as DeleteCustomWorldAgentSessionProcedure from "./delete_custom_world_agent_session_procedure";
import * as DeleteCustomWorldProfileAndReturnProcedure from "./delete_custom_world_profile_and_return_procedure";
import * as DeletePuzzleWorkProcedure from "./delete_puzzle_work_procedure";
import * as DeleteRuntimeSnapshotAndReturnProcedure from "./delete_runtime_snapshot_and_return_procedure";
import * as DragPuzzlePieceOrGroupProcedure from "./drag_puzzle_piece_or_group_procedure";
import * as ExecuteCustomWorldAgentActionProcedure from "./execute_custom_world_agent_action_procedure";
import * as ExportAuthStoreSnapshotFromTablesProcedure from "./export_auth_store_snapshot_from_tables_procedure";
import * as FailAiTaskAndReturnProcedure from "./fail_ai_task_and_return_procedure";
import * as FinalizeBigFishAgentMessageTurnProcedure from "./finalize_big_fish_agent_message_turn_procedure";
import * as FinalizeCustomWorldAgentMessageTurnProcedure from "./finalize_custom_world_agent_message_turn_procedure";
import * as FinalizePuzzleAgentMessageTurnProcedure from "./finalize_puzzle_agent_message_turn_procedure";
import * as GenerateBigFishAssetProcedure from "./generate_big_fish_asset_procedure";
import * as GetAuthStoreSnapshotProcedure from "./get_auth_store_snapshot_procedure";
import * as GetBattleStateProcedure from "./get_battle_state_procedure";
import * as GetBigFishRunProcedure from "./get_big_fish_run_procedure";
import * as GetBigFishSessionProcedure from "./get_big_fish_session_procedure";
@@ -95,10 +103,12 @@ import * as GetCustomWorldAgentCardDetailProcedure from "./get_custom_world_agen
import * as GetCustomWorldAgentOperationProcedure from "./get_custom_world_agent_operation_procedure";
import * as GetCustomWorldAgentSessionProcedure from "./get_custom_world_agent_session_procedure";
import * as GetCustomWorldGalleryDetailProcedure from "./get_custom_world_gallery_detail_procedure";
import * as GetCustomWorldGalleryDetailByCodeProcedure from "./get_custom_world_gallery_detail_by_code_procedure";
import * as GetCustomWorldLibraryDetailProcedure from "./get_custom_world_library_detail_procedure";
import * as GetPlayerProgressionOrDefaultProcedure from "./get_player_progression_or_default_procedure";
import * as GetProfileDashboardProcedure from "./get_profile_dashboard_procedure";
import * as GetProfilePlayStatsProcedure from "./get_profile_play_stats_procedure";
import * as GetProfileRechargeCenterProcedure from "./get_profile_recharge_center_procedure";
import * as GetPuzzleAgentSessionProcedure from "./get_puzzle_agent_session_procedure";
import * as GetPuzzleGalleryDetailProcedure from "./get_puzzle_gallery_detail_procedure";
import * as GetPuzzleRunProcedure from "./get_puzzle_run_procedure";
@@ -108,6 +118,8 @@ import * as GetRuntimeSettingOrDefaultProcedure from "./get_runtime_setting_or_d
import * as GetRuntimeSnapshotProcedure from "./get_runtime_snapshot_procedure";
import * as GetStorySessionStateProcedure from "./get_story_session_state_procedure";
import * as GrantPlayerProgressionExperienceAndReturnProcedure from "./grant_player_progression_experience_and_return_procedure";
import * as ImportAuthStoreSnapshotProcedure from "./import_auth_store_snapshot_procedure";
import * as ListBigFishWorksProcedure from "./list_big_fish_works_procedure";
import * as ListCustomWorldGalleryEntriesProcedure from "./list_custom_world_gallery_entries_procedure";
import * as ListCustomWorldProfilesProcedure from "./list_custom_world_profiles_procedure";
import * as ListCustomWorldWorksProcedure from "./list_custom_world_works_procedure";
@@ -137,7 +149,9 @@ import * as SubmitPuzzleAgentMessageProcedure from "./submit_puzzle_agent_messag
import * as SwapPuzzlePiecesProcedure from "./swap_puzzle_pieces_procedure";
import * as UnpublishCustomWorldProfileAndReturnProcedure from "./unpublish_custom_world_profile_and_return_procedure";
import * as UpdatePuzzleWorkProcedure from "./update_puzzle_work_procedure";
import * as UpsertAuthStoreSnapshotProcedure from "./upsert_auth_store_snapshot_procedure";
import * as UpsertChapterProgressionAndReturnProcedure from "./upsert_chapter_progression_and_return_procedure";
import * as UpsertCustomWorldAgentOperationProgressProcedure from "./upsert_custom_world_agent_operation_progress_procedure";
import * as UpsertCustomWorldProfileAndReturnProcedure from "./upsert_custom_world_profile_and_return_procedure";
import * as UpsertNpcStateAndReturnProcedure from "./upsert_npc_state_and_return_procedure";
import * as UpsertPlatformBrowseHistoryAndReturnProcedure from "./upsert_platform_browse_history_and_return_procedure";
@@ -160,6 +174,9 @@ const tablesSchema = __schema({
{ accessor: 'profile_id', name: 'custom_world_gallery_entry_profile_id_idx_btree', algorithm: 'btree', columns: [
'profileId',
] },
{ accessor: 'by_custom_world_gallery_public_work_code', name: 'custom_world_gallery_entry_public_work_code_idx_btree', algorithm: 'btree', columns: [
'publicWorkCode',
] },
{ accessor: 'by_custom_world_gallery_theme_mode', name: 'custom_world_gallery_entry_theme_mode_idx_btree', algorithm: 'btree', columns: [
'themeMode',
] },
@@ -219,14 +236,22 @@ const proceduresSchema = __procedures(
__procedureSchema("create_battle_state_and_return", CreateBattleStateAndReturnProcedure.params, CreateBattleStateAndReturnProcedure.returnType),
__procedureSchema("create_big_fish_session", CreateBigFishSessionProcedure.params, CreateBigFishSessionProcedure.returnType),
__procedureSchema("create_custom_world_agent_session", CreateCustomWorldAgentSessionProcedure.params, CreateCustomWorldAgentSessionProcedure.returnType),
__procedureSchema("create_profile_recharge_order_and_return", CreateProfileRechargeOrderAndReturnProcedure.params, CreateProfileRechargeOrderAndReturnProcedure.returnType),
__procedureSchema("create_puzzle_agent_session", CreatePuzzleAgentSessionProcedure.params, CreatePuzzleAgentSessionProcedure.returnType),
__procedureSchema("delete_big_fish_work", DeleteBigFishWorkProcedure.params, DeleteBigFishWorkProcedure.returnType),
__procedureSchema("delete_custom_world_agent_session", DeleteCustomWorldAgentSessionProcedure.params, DeleteCustomWorldAgentSessionProcedure.returnType),
__procedureSchema("delete_custom_world_profile_and_return", DeleteCustomWorldProfileAndReturnProcedure.params, DeleteCustomWorldProfileAndReturnProcedure.returnType),
__procedureSchema("delete_puzzle_work", DeletePuzzleWorkProcedure.params, DeletePuzzleWorkProcedure.returnType),
__procedureSchema("delete_runtime_snapshot_and_return", DeleteRuntimeSnapshotAndReturnProcedure.params, DeleteRuntimeSnapshotAndReturnProcedure.returnType),
__procedureSchema("drag_puzzle_piece_or_group", DragPuzzlePieceOrGroupProcedure.params, DragPuzzlePieceOrGroupProcedure.returnType),
__procedureSchema("execute_custom_world_agent_action", ExecuteCustomWorldAgentActionProcedure.params, ExecuteCustomWorldAgentActionProcedure.returnType),
__procedureSchema("export_auth_store_snapshot_from_tables", ExportAuthStoreSnapshotFromTablesProcedure.params, ExportAuthStoreSnapshotFromTablesProcedure.returnType),
__procedureSchema("fail_ai_task_and_return", FailAiTaskAndReturnProcedure.params, FailAiTaskAndReturnProcedure.returnType),
__procedureSchema("finalize_big_fish_agent_message_turn", FinalizeBigFishAgentMessageTurnProcedure.params, FinalizeBigFishAgentMessageTurnProcedure.returnType),
__procedureSchema("finalize_custom_world_agent_message_turn", FinalizeCustomWorldAgentMessageTurnProcedure.params, FinalizeCustomWorldAgentMessageTurnProcedure.returnType),
__procedureSchema("finalize_puzzle_agent_message_turn", FinalizePuzzleAgentMessageTurnProcedure.params, FinalizePuzzleAgentMessageTurnProcedure.returnType),
__procedureSchema("generate_big_fish_asset", GenerateBigFishAssetProcedure.params, GenerateBigFishAssetProcedure.returnType),
__procedureSchema("get_auth_store_snapshot", GetAuthStoreSnapshotProcedure.params, GetAuthStoreSnapshotProcedure.returnType),
__procedureSchema("get_battle_state", GetBattleStateProcedure.params, GetBattleStateProcedure.returnType),
__procedureSchema("get_big_fish_run", GetBigFishRunProcedure.params, GetBigFishRunProcedure.returnType),
__procedureSchema("get_big_fish_session", GetBigFishSessionProcedure.params, GetBigFishSessionProcedure.returnType),
@@ -235,10 +260,12 @@ const proceduresSchema = __procedures(
__procedureSchema("get_custom_world_agent_operation", GetCustomWorldAgentOperationProcedure.params, GetCustomWorldAgentOperationProcedure.returnType),
__procedureSchema("get_custom_world_agent_session", GetCustomWorldAgentSessionProcedure.params, GetCustomWorldAgentSessionProcedure.returnType),
__procedureSchema("get_custom_world_gallery_detail", GetCustomWorldGalleryDetailProcedure.params, GetCustomWorldGalleryDetailProcedure.returnType),
__procedureSchema("get_custom_world_gallery_detail_by_code", GetCustomWorldGalleryDetailByCodeProcedure.params, GetCustomWorldGalleryDetailByCodeProcedure.returnType),
__procedureSchema("get_custom_world_library_detail", GetCustomWorldLibraryDetailProcedure.params, GetCustomWorldLibraryDetailProcedure.returnType),
__procedureSchema("get_player_progression_or_default", GetPlayerProgressionOrDefaultProcedure.params, GetPlayerProgressionOrDefaultProcedure.returnType),
__procedureSchema("get_profile_dashboard", GetProfileDashboardProcedure.params, GetProfileDashboardProcedure.returnType),
__procedureSchema("get_profile_play_stats", GetProfilePlayStatsProcedure.params, GetProfilePlayStatsProcedure.returnType),
__procedureSchema("get_profile_recharge_center", GetProfileRechargeCenterProcedure.params, GetProfileRechargeCenterProcedure.returnType),
__procedureSchema("get_puzzle_agent_session", GetPuzzleAgentSessionProcedure.params, GetPuzzleAgentSessionProcedure.returnType),
__procedureSchema("get_puzzle_gallery_detail", GetPuzzleGalleryDetailProcedure.params, GetPuzzleGalleryDetailProcedure.returnType),
__procedureSchema("get_puzzle_run", GetPuzzleRunProcedure.params, GetPuzzleRunProcedure.returnType),
@@ -248,6 +275,8 @@ const proceduresSchema = __procedures(
__procedureSchema("get_runtime_snapshot", GetRuntimeSnapshotProcedure.params, GetRuntimeSnapshotProcedure.returnType),
__procedureSchema("get_story_session_state", GetStorySessionStateProcedure.params, GetStorySessionStateProcedure.returnType),
__procedureSchema("grant_player_progression_experience_and_return", GrantPlayerProgressionExperienceAndReturnProcedure.params, GrantPlayerProgressionExperienceAndReturnProcedure.returnType),
__procedureSchema("import_auth_store_snapshot", ImportAuthStoreSnapshotProcedure.params, ImportAuthStoreSnapshotProcedure.returnType),
__procedureSchema("list_big_fish_works", ListBigFishWorksProcedure.params, ListBigFishWorksProcedure.returnType),
__procedureSchema("list_custom_world_gallery_entries", ListCustomWorldGalleryEntriesProcedure.params, ListCustomWorldGalleryEntriesProcedure.returnType),
__procedureSchema("list_custom_world_profiles", ListCustomWorldProfilesProcedure.params, ListCustomWorldProfilesProcedure.returnType),
__procedureSchema("list_custom_world_works", ListCustomWorldWorksProcedure.params, ListCustomWorldWorksProcedure.returnType),
@@ -277,7 +306,9 @@ const proceduresSchema = __procedures(
__procedureSchema("swap_puzzle_pieces", SwapPuzzlePiecesProcedure.params, SwapPuzzlePiecesProcedure.returnType),
__procedureSchema("unpublish_custom_world_profile_and_return", UnpublishCustomWorldProfileAndReturnProcedure.params, UnpublishCustomWorldProfileAndReturnProcedure.returnType),
__procedureSchema("update_puzzle_work", UpdatePuzzleWorkProcedure.params, UpdatePuzzleWorkProcedure.returnType),
__procedureSchema("upsert_auth_store_snapshot", UpsertAuthStoreSnapshotProcedure.params, UpsertAuthStoreSnapshotProcedure.returnType),
__procedureSchema("upsert_chapter_progression_and_return", UpsertChapterProgressionAndReturnProcedure.params, UpsertChapterProgressionAndReturnProcedure.returnType),
__procedureSchema("upsert_custom_world_agent_operation_progress", UpsertCustomWorldAgentOperationProgressProcedure.params, UpsertCustomWorldAgentOperationProgressProcedure.returnType),
__procedureSchema("upsert_custom_world_profile_and_return", UpsertCustomWorldProfileAndReturnProcedure.params, UpsertCustomWorldProfileAndReturnProcedure.returnType),
__procedureSchema("upsert_npc_state_and_return", UpsertNpcStateAndReturnProcedure.params, UpsertNpcStateAndReturnProcedure.returnType),
__procedureSchema("upsert_platform_browse_history_and_return", UpsertPlatformBrowseHistoryAndReturnProcedure.params, UpsertPlatformBrowseHistoryAndReturnProcedure.returnType),

View File

@@ -0,0 +1,23 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
import {
BigFishWorksProcedureResult,
BigFishWorksListInput,
} from "./types";
export const params = {
get input() {
return BigFishWorksListInput;
},
};
export const returnType = BigFishWorksProcedureResult

View File

@@ -11,8 +11,8 @@ import {
} from "spacetimedb";
import {
CustomWorldWorksListInput,
CustomWorldWorksListResult,
CustomWorldWorksListInput,
} from "./types";
export const params = {

View File

@@ -438,6 +438,62 @@ export const AssetObjectUpsertSnapshot = __t.object("AssetObjectUpsertSnapshot",
});
export type AssetObjectUpsertSnapshot = __Infer<typeof AssetObjectUpsertSnapshot>;
export const AuthIdentity = __t.object("AuthIdentity", {
identityId: __t.string(),
userId: __t.string(),
provider: __t.string(),
providerUid: __t.string(),
providerUnionId: __t.option(__t.string()),
phoneE164: __t.option(__t.string()),
displayName: __t.option(__t.string()),
avatarUrl: __t.option(__t.string()),
});
export type AuthIdentity = __Infer<typeof AuthIdentity>;
export const AuthStoreSnapshot = __t.object("AuthStoreSnapshot", {
snapshotId: __t.string(),
snapshotJson: __t.string(),
updatedAt: __t.timestamp(),
});
export type AuthStoreSnapshot = __Infer<typeof AuthStoreSnapshot>;
export const AuthStoreSnapshotImportProcedureResult = __t.object("AuthStoreSnapshotImportProcedureResult", {
ok: __t.bool(),
get record() {
return __t.option(AuthStoreSnapshotImportRecord);
},
errorMessage: __t.option(__t.string()),
});
export type AuthStoreSnapshotImportProcedureResult = __Infer<typeof AuthStoreSnapshotImportProcedureResult>;
export const AuthStoreSnapshotImportRecord = __t.object("AuthStoreSnapshotImportRecord", {
importedUserCount: __t.u32(),
importedIdentityCount: __t.u32(),
importedRefreshSessionCount: __t.u32(),
});
export type AuthStoreSnapshotImportRecord = __Infer<typeof AuthStoreSnapshotImportRecord>;
export const AuthStoreSnapshotProcedureResult = __t.object("AuthStoreSnapshotProcedureResult", {
ok: __t.bool(),
get record() {
return __t.option(AuthStoreSnapshotRecord);
},
errorMessage: __t.option(__t.string()),
});
export type AuthStoreSnapshotProcedureResult = __Infer<typeof AuthStoreSnapshotProcedureResult>;
export const AuthStoreSnapshotRecord = __t.object("AuthStoreSnapshotRecord", {
snapshotJson: __t.option(__t.string()),
updatedAtMicros: __t.option(__t.i64()),
});
export type AuthStoreSnapshotRecord = __Infer<typeof AuthStoreSnapshotRecord>;
export const AuthStoreSnapshotUpsertInput = __t.object("AuthStoreSnapshotUpsertInput", {
snapshotJson: __t.string(),
updatedAtMicros: __t.i64(),
});
export type AuthStoreSnapshotUpsertInput = __Infer<typeof AuthStoreSnapshotUpsertInput>;
// The tagged union or sum type for the algebraic type `BattleMode`.
export const BattleMode = __t.enum("BattleMode", {
Fight: __t.unit(),
@@ -802,6 +858,21 @@ export const BigFishLevelBlueprint = __t.object("BigFishLevelBlueprint", {
});
export type BigFishLevelBlueprint = __Infer<typeof BigFishLevelBlueprint>;
export const BigFishMessageFinalizeInput = __t.object("BigFishMessageFinalizeInput", {
sessionId: __t.string(),
ownerUserId: __t.string(),
assistantMessageId: __t.option(__t.string()),
assistantReplyText: __t.option(__t.string()),
get stage() {
return BigFishCreationStage;
},
progressPercent: __t.u32(),
anchorPackJson: __t.string(),
errorMessage: __t.option(__t.string()),
updatedAtMicros: __t.i64(),
});
export type BigFishMessageFinalizeInput = __Infer<typeof BigFishMessageFinalizeInput>;
export const BigFishMessageSubmitInput = __t.object("BigFishMessageSubmitInput", {
sessionId: __t.string(),
ownerUserId: __t.string(),
@@ -988,6 +1059,24 @@ export const BigFishVector2 = __t.object("BigFishVector2", {
});
export type BigFishVector2 = __Infer<typeof BigFishVector2>;
export const BigFishWorkDeleteInput = __t.object("BigFishWorkDeleteInput", {
sessionId: __t.string(),
ownerUserId: __t.string(),
});
export type BigFishWorkDeleteInput = __Infer<typeof BigFishWorkDeleteInput>;
export const BigFishWorksListInput = __t.object("BigFishWorksListInput", {
ownerUserId: __t.string(),
});
export type BigFishWorksListInput = __Infer<typeof BigFishWorksListInput>;
export const BigFishWorksProcedureResult = __t.object("BigFishWorksProcedureResult", {
ok: __t.bool(),
itemsJson: __t.option(__t.string()),
errorMessage: __t.option(__t.string()),
});
export type BigFishWorksProcedureResult = __Infer<typeof BigFishWorksProcedureResult>;
// The tagged union or sum type for the algebraic type `ChapterPaceBand`.
export const ChapterPaceBand = __t.enum("ChapterPaceBand", {
OpeningFast: __t.unit(),
@@ -1157,8 +1246,8 @@ export const CustomWorldAgentMessageFinalizeInput = __t.object("CustomWorldAgent
sessionId: __t.string(),
ownerUserId: __t.string(),
operationId: __t.string(),
assistantMessageId: __t.string(),
assistantReplyText: __t.string(),
assistantMessageId: __t.option(__t.string()),
assistantReplyText: __t.option(__t.string()),
phaseLabel: __t.string(),
phaseDetail: __t.string(),
get operationStatus() {
@@ -1244,6 +1333,24 @@ export const CustomWorldAgentOperationProcedureResult = __t.object("CustomWorldA
});
export type CustomWorldAgentOperationProcedureResult = __Infer<typeof CustomWorldAgentOperationProcedureResult>;
export const CustomWorldAgentOperationProgressInput = __t.object("CustomWorldAgentOperationProgressInput", {
sessionId: __t.string(),
ownerUserId: __t.string(),
operationId: __t.string(),
get operationType() {
return RpgAgentOperationType;
},
get operationStatus() {
return RpgAgentOperationStatus;
},
phaseLabel: __t.string(),
phaseDetail: __t.string(),
operationProgress: __t.u32(),
errorMessage: __t.option(__t.string()),
updatedAtMicros: __t.i64(),
});
export type CustomWorldAgentOperationProgressInput = __Infer<typeof CustomWorldAgentOperationProgressInput>;
export const CustomWorldAgentOperationSnapshot = __t.object("CustomWorldAgentOperationSnapshot", {
operationId: __t.string(),
sessionId: __t.string(),
@@ -1454,6 +1561,11 @@ export const CustomWorldDraftCardSnapshot = __t.object("CustomWorldDraftCardSnap
});
export type CustomWorldDraftCardSnapshot = __Infer<typeof CustomWorldDraftCardSnapshot>;
export const CustomWorldGalleryDetailByCodeInput = __t.object("CustomWorldGalleryDetailByCodeInput", {
publicWorkCode: __t.string(),
});
export type CustomWorldGalleryDetailByCodeInput = __Infer<typeof CustomWorldGalleryDetailByCodeInput>;
export const CustomWorldGalleryDetailInput = __t.object("CustomWorldGalleryDetailInput", {
ownerUserId: __t.string(),
profileId: __t.string(),
@@ -1463,6 +1575,8 @@ export type CustomWorldGalleryDetailInput = __Infer<typeof CustomWorldGalleryDet
export const CustomWorldGalleryEntry = __t.object("CustomWorldGalleryEntry", {
profileId: __t.string(),
ownerUserId: __t.string(),
publicWorkCode: __t.string(),
authorPublicUserCode: __t.string(),
authorDisplayName: __t.string(),
worldName: __t.string(),
subtitle: __t.string(),
@@ -1481,6 +1595,8 @@ export type CustomWorldGalleryEntry = __Infer<typeof CustomWorldGalleryEntry>;
export const CustomWorldGalleryEntrySnapshot = __t.object("CustomWorldGalleryEntrySnapshot", {
profileId: __t.string(),
ownerUserId: __t.string(),
publicWorkCode: __t.string(),
authorPublicUserCode: __t.string(),
authorDisplayName: __t.string(),
worldName: __t.string(),
subtitle: __t.string(),
@@ -1533,6 +1649,8 @@ export type CustomWorldLibraryMutationResult = __Infer<typeof CustomWorldLibrary
export const CustomWorldProfile = __t.object("CustomWorldProfile", {
profileId: __t.string(),
ownerUserId: __t.string(),
publicWorkCode: __t.option(__t.string()),
authorPublicUserCode: __t.option(__t.string()),
sourceAgentSessionId: __t.option(__t.string()),
get publicationStatus() {
return CustomWorldPublicationStatus;
@@ -1579,6 +1697,8 @@ export type CustomWorldProfileListResult = __Infer<typeof CustomWorldProfileList
export const CustomWorldProfilePublishInput = __t.object("CustomWorldProfilePublishInput", {
profileId: __t.string(),
ownerUserId: __t.string(),
publicWorkCode: __t.option(__t.string()),
authorPublicUserCode: __t.string(),
authorDisplayName: __t.string(),
publishedAtMicros: __t.i64(),
});
@@ -1587,6 +1707,8 @@ export type CustomWorldProfilePublishInput = __Infer<typeof CustomWorldProfilePu
export const CustomWorldProfileSnapshot = __t.object("CustomWorldProfileSnapshot", {
profileId: __t.string(),
ownerUserId: __t.string(),
publicWorkCode: __t.option(__t.string()),
authorPublicUserCode: __t.option(__t.string()),
sourceAgentSessionId: __t.option(__t.string()),
get publicationStatus() {
return CustomWorldPublicationStatus;
@@ -1620,6 +1742,8 @@ export type CustomWorldProfileUnpublishInput = __Infer<typeof CustomWorldProfile
export const CustomWorldProfileUpsertInput = __t.object("CustomWorldProfileUpsertInput", {
profileId: __t.string(),
ownerUserId: __t.string(),
publicWorkCode: __t.option(__t.string()),
authorPublicUserCode: __t.option(__t.string()),
sourceAgentSessionId: __t.option(__t.string()),
worldName: __t.string(),
subtitle: __t.string(),
@@ -1647,6 +1771,8 @@ export const CustomWorldPublishWorldInput = __t.object("CustomWorldPublishWorldI
sessionId: __t.string(),
profileId: __t.string(),
ownerUserId: __t.string(),
publicWorkCode: __t.option(__t.string()),
authorPublicUserCode: __t.string(),
draftProfileJson: __t.string(),
legacyResultProfileJson: __t.option(__t.string()),
settingText: __t.string(),
@@ -2230,6 +2356,20 @@ export const ProfileDashboardState = __t.object("ProfileDashboardState", {
});
export type ProfileDashboardState = __Infer<typeof ProfileDashboardState>;
export const ProfileMembership = __t.object("ProfileMembership", {
userId: __t.string(),
get status() {
return RuntimeProfileMembershipStatus;
},
get tier() {
return RuntimeProfileMembershipTier;
},
startedAt: __t.timestamp(),
expiresAt: __t.timestamp(),
updatedAt: __t.timestamp(),
});
export type ProfileMembership = __Infer<typeof ProfileMembership>;
export const ProfilePlayedWorld = __t.object("ProfilePlayedWorld", {
playedWorldId: __t.string(),
userId: __t.string(),
@@ -2245,6 +2385,26 @@ export const ProfilePlayedWorld = __t.object("ProfilePlayedWorld", {
});
export type ProfilePlayedWorld = __Infer<typeof ProfilePlayedWorld>;
export const ProfileRechargeOrder = __t.object("ProfileRechargeOrder", {
orderId: __t.string(),
userId: __t.string(),
productId: __t.string(),
productTitle: __t.string(),
get kind() {
return RuntimeProfileRechargeProductKind;
},
amountCents: __t.u64(),
get status() {
return RuntimeProfileRechargeOrderStatus;
},
paymentChannel: __t.string(),
paidAt: __t.timestamp(),
createdAt: __t.timestamp(),
pointsDelta: __t.i64(),
membershipExpiresAt: __t.option(__t.timestamp()),
});
export type ProfileRechargeOrder = __Infer<typeof ProfileRechargeOrder>;
export const ProfileSaveArchive = __t.object("ProfileSaveArchive", {
archiveId: __t.string(),
userId: __t.string(),
@@ -2277,6 +2437,21 @@ export const ProfileWalletLedger = __t.object("ProfileWalletLedger", {
});
export type ProfileWalletLedger = __Infer<typeof ProfileWalletLedger>;
export const PuzzleAgentMessageFinalizeInput = __t.object("PuzzleAgentMessageFinalizeInput", {
sessionId: __t.string(),
ownerUserId: __t.string(),
assistantMessageId: __t.option(__t.string()),
assistantReplyText: __t.option(__t.string()),
get stage() {
return PuzzleAgentStage;
},
progressPercent: __t.u32(),
anchorPackJson: __t.string(),
errorMessage: __t.option(__t.string()),
updatedAtMicros: __t.i64(),
});
export type PuzzleAgentMessageFinalizeInput = __Infer<typeof PuzzleAgentMessageFinalizeInput>;
// The tagged union or sum type for the algebraic type `PuzzleAgentMessageKind`.
export const PuzzleAgentMessageKind = __t.enum("PuzzleAgentMessageKind", {
Chat: __t.unit(),
@@ -2474,6 +2649,12 @@ export const PuzzleSelectCoverImageInput = __t.object("PuzzleSelectCoverImageInp
});
export type PuzzleSelectCoverImageInput = __Infer<typeof PuzzleSelectCoverImageInput>;
export const PuzzleWorkDeleteInput = __t.object("PuzzleWorkDeleteInput", {
profileId: __t.string(),
ownerUserId: __t.string(),
});
export type PuzzleWorkDeleteInput = __Infer<typeof PuzzleWorkDeleteInput>;
export const PuzzleWorkGetInput = __t.object("PuzzleWorkGetInput", {
profileId: __t.string(),
});
@@ -2879,6 +3060,20 @@ export const QuestTurnInInput = __t.object("QuestTurnInInput", {
});
export type QuestTurnInInput = __Infer<typeof QuestTurnInInput>;
export const RefreshSession = __t.object("RefreshSession", {
sessionId: __t.string(),
userId: __t.string(),
refreshTokenHash: __t.string(),
issuedByProvider: __t.string(),
clientInfoJson: __t.string(),
expiresAt: __t.string(),
revokedAt: __t.option(__t.string()),
createdAt: __t.string(),
updatedAt: __t.string(),
lastSeenAt: __t.string(),
});
export type RefreshSession = __Infer<typeof RefreshSession>;
export const ResolveCombatActionInput = __t.object("ResolveCombatActionInput", {
battleStateId: __t.string(),
functionId: __t.string(),
@@ -3016,6 +3211,8 @@ export const RpgAgentOperationType = __t.enum("RpgAgentOperationType", {
SyncResultProfile: __t.unit(),
GenerateCharacters: __t.unit(),
GenerateLandmarks: __t.unit(),
DeleteCharacters: __t.unit(),
DeleteLandmarks: __t.unit(),
GenerateRoleAssets: __t.unit(),
SyncRoleAssets: __t.unit(),
GenerateSceneAssets: __t.unit(),
@@ -3204,6 +3401,45 @@ export const RuntimeProfileDashboardSnapshot = __t.object("RuntimeProfileDashboa
});
export type RuntimeProfileDashboardSnapshot = __Infer<typeof RuntimeProfileDashboardSnapshot>;
export const RuntimeProfileMembershipBenefitSnapshot = __t.object("RuntimeProfileMembershipBenefitSnapshot", {
benefitName: __t.string(),
normalValue: __t.string(),
monthValue: __t.string(),
seasonValue: __t.string(),
yearValue: __t.string(),
});
export type RuntimeProfileMembershipBenefitSnapshot = __Infer<typeof RuntimeProfileMembershipBenefitSnapshot>;
export const RuntimeProfileMembershipSnapshot = __t.object("RuntimeProfileMembershipSnapshot", {
userId: __t.string(),
get status() {
return RuntimeProfileMembershipStatus;
},
get tier() {
return RuntimeProfileMembershipTier;
},
startedAtMicros: __t.option(__t.i64()),
expiresAtMicros: __t.option(__t.i64()),
updatedAtMicros: __t.option(__t.i64()),
});
export type RuntimeProfileMembershipSnapshot = __Infer<typeof RuntimeProfileMembershipSnapshot>;
// The tagged union or sum type for the algebraic type `RuntimeProfileMembershipStatus`.
export const RuntimeProfileMembershipStatus = __t.enum("RuntimeProfileMembershipStatus", {
Normal: __t.unit(),
Active: __t.unit(),
});
export type RuntimeProfileMembershipStatus = __Infer<typeof RuntimeProfileMembershipStatus>;
// The tagged union or sum type for the algebraic type `RuntimeProfileMembershipTier`.
export const RuntimeProfileMembershipTier = __t.enum("RuntimeProfileMembershipTier", {
Normal: __t.unit(),
Month: __t.unit(),
Season: __t.unit(),
Year: __t.unit(),
});
export type RuntimeProfileMembershipTier = __Infer<typeof RuntimeProfileMembershipTier>;
export const RuntimeProfilePlayStatsGetInput = __t.object("RuntimeProfilePlayStatsGetInput", {
userId: __t.string(),
});
@@ -3243,6 +3479,104 @@ export const RuntimeProfilePlayedWorldSnapshot = __t.object("RuntimeProfilePlaye
});
export type RuntimeProfilePlayedWorldSnapshot = __Infer<typeof RuntimeProfilePlayedWorldSnapshot>;
export const RuntimeProfileRechargeCenterGetInput = __t.object("RuntimeProfileRechargeCenterGetInput", {
userId: __t.string(),
});
export type RuntimeProfileRechargeCenterGetInput = __Infer<typeof RuntimeProfileRechargeCenterGetInput>;
export const RuntimeProfileRechargeCenterProcedureResult = __t.object("RuntimeProfileRechargeCenterProcedureResult", {
ok: __t.bool(),
get record() {
return __t.option(RuntimeProfileRechargeCenterSnapshot);
},
get order() {
return __t.option(RuntimeProfileRechargeOrderSnapshot);
},
errorMessage: __t.option(__t.string()),
});
export type RuntimeProfileRechargeCenterProcedureResult = __Infer<typeof RuntimeProfileRechargeCenterProcedureResult>;
export const RuntimeProfileRechargeCenterSnapshot = __t.object("RuntimeProfileRechargeCenterSnapshot", {
userId: __t.string(),
walletBalance: __t.u64(),
get membership() {
return RuntimeProfileMembershipSnapshot;
},
get pointProducts() {
return __t.array(RuntimeProfileRechargeProductSnapshot);
},
get membershipProducts() {
return __t.array(RuntimeProfileRechargeProductSnapshot);
},
get benefits() {
return __t.array(RuntimeProfileMembershipBenefitSnapshot);
},
get latestOrder() {
return __t.option(RuntimeProfileRechargeOrderSnapshot);
},
hasPointsRecharged: __t.bool(),
});
export type RuntimeProfileRechargeCenterSnapshot = __Infer<typeof RuntimeProfileRechargeCenterSnapshot>;
export const RuntimeProfileRechargeOrderCreateInput = __t.object("RuntimeProfileRechargeOrderCreateInput", {
userId: __t.string(),
productId: __t.string(),
paymentChannel: __t.string(),
createdAtMicros: __t.i64(),
});
export type RuntimeProfileRechargeOrderCreateInput = __Infer<typeof RuntimeProfileRechargeOrderCreateInput>;
export const RuntimeProfileRechargeOrderSnapshot = __t.object("RuntimeProfileRechargeOrderSnapshot", {
orderId: __t.string(),
userId: __t.string(),
productId: __t.string(),
productTitle: __t.string(),
get kind() {
return RuntimeProfileRechargeProductKind;
},
amountCents: __t.u64(),
get status() {
return RuntimeProfileRechargeOrderStatus;
},
paymentChannel: __t.string(),
paidAtMicros: __t.i64(),
createdAtMicros: __t.i64(),
pointsDelta: __t.i64(),
membershipExpiresAtMicros: __t.option(__t.i64()),
});
export type RuntimeProfileRechargeOrderSnapshot = __Infer<typeof RuntimeProfileRechargeOrderSnapshot>;
// The tagged union or sum type for the algebraic type `RuntimeProfileRechargeOrderStatus`.
export const RuntimeProfileRechargeOrderStatus = __t.enum("RuntimeProfileRechargeOrderStatus", {
Paid: __t.unit(),
});
export type RuntimeProfileRechargeOrderStatus = __Infer<typeof RuntimeProfileRechargeOrderStatus>;
// The tagged union or sum type for the algebraic type `RuntimeProfileRechargeProductKind`.
export const RuntimeProfileRechargeProductKind = __t.enum("RuntimeProfileRechargeProductKind", {
Points: __t.unit(),
Membership: __t.unit(),
});
export type RuntimeProfileRechargeProductKind = __Infer<typeof RuntimeProfileRechargeProductKind>;
export const RuntimeProfileRechargeProductSnapshot = __t.object("RuntimeProfileRechargeProductSnapshot", {
productId: __t.string(),
title: __t.string(),
priceCents: __t.u64(),
get kind() {
return RuntimeProfileRechargeProductKind;
},
pointsAmount: __t.u64(),
bonusPoints: __t.u64(),
durationDays: __t.u32(),
badgeLabel: __t.string(),
description: __t.string(),
get tier() {
return RuntimeProfileMembershipTier;
},
});
export type RuntimeProfileRechargeProductSnapshot = __Infer<typeof RuntimeProfileRechargeProductSnapshot>;
export const RuntimeProfileSaveArchiveListInput = __t.object("RuntimeProfileSaveArchiveListInput", {
userId: __t.string(),
});
@@ -3318,6 +3652,9 @@ export type RuntimeProfileWalletLedgerProcedureResult = __Infer<typeof RuntimePr
// The tagged union or sum type for the algebraic type `RuntimeProfileWalletLedgerSourceType`.
export const RuntimeProfileWalletLedgerSourceType = __t.enum("RuntimeProfileWalletLedgerSourceType", {
SnapshotSync: __t.unit(),
InviteInviterReward: __t.unit(),
InviteInviteeReward: __t.unit(),
PointsRecharge: __t.unit(),
});
export type RuntimeProfileWalletLedgerSourceType = __Infer<typeof RuntimeProfileWalletLedgerSourceType>;
@@ -3638,6 +3975,22 @@ export const UnequipInventoryItemInput = __t.object("UnequipInventoryItemInput",
});
export type UnequipInventoryItemInput = __Infer<typeof UnequipInventoryItemInput>;
export const UserAccount = __t.object("UserAccount", {
userId: __t.string(),
publicUserCode: __t.string(),
username: __t.string(),
displayName: __t.string(),
phoneNumberMasked: __t.option(__t.string()),
phoneNumberE164: __t.option(__t.string()),
loginMethod: __t.string(),
bindingStatus: __t.string(),
wechatBound: __t.bool(),
passwordHash: __t.string(),
passwordLoginEnabled: __t.bool(),
tokenVersion: __t.u64(),
});
export type UserAccount = __Infer<typeof UserAccount>;
export const UserBrowseHistory = __t.object("UserBrowseHistory", {
browseHistoryId: __t.string(),
userId: __t.string(),

View File

@@ -25,14 +25,22 @@ import * as CreateAiTaskAndReturnProcedure from "../create_ai_task_and_return_pr
import * as CreateBattleStateAndReturnProcedure from "../create_battle_state_and_return_procedure";
import * as CreateBigFishSessionProcedure from "../create_big_fish_session_procedure";
import * as CreateCustomWorldAgentSessionProcedure from "../create_custom_world_agent_session_procedure";
import * as CreateProfileRechargeOrderAndReturnProcedure from "../create_profile_recharge_order_and_return_procedure";
import * as CreatePuzzleAgentSessionProcedure from "../create_puzzle_agent_session_procedure";
import * as DeleteBigFishWorkProcedure from "../delete_big_fish_work_procedure";
import * as DeleteCustomWorldAgentSessionProcedure from "../delete_custom_world_agent_session_procedure";
import * as DeleteCustomWorldProfileAndReturnProcedure from "../delete_custom_world_profile_and_return_procedure";
import * as DeletePuzzleWorkProcedure from "../delete_puzzle_work_procedure";
import * as DeleteRuntimeSnapshotAndReturnProcedure from "../delete_runtime_snapshot_and_return_procedure";
import * as DragPuzzlePieceOrGroupProcedure from "../drag_puzzle_piece_or_group_procedure";
import * as ExecuteCustomWorldAgentActionProcedure from "../execute_custom_world_agent_action_procedure";
import * as ExportAuthStoreSnapshotFromTablesProcedure from "../export_auth_store_snapshot_from_tables_procedure";
import * as FailAiTaskAndReturnProcedure from "../fail_ai_task_and_return_procedure";
import * as FinalizeBigFishAgentMessageTurnProcedure from "../finalize_big_fish_agent_message_turn_procedure";
import * as FinalizeCustomWorldAgentMessageTurnProcedure from "../finalize_custom_world_agent_message_turn_procedure";
import * as FinalizePuzzleAgentMessageTurnProcedure from "../finalize_puzzle_agent_message_turn_procedure";
import * as GenerateBigFishAssetProcedure from "../generate_big_fish_asset_procedure";
import * as GetAuthStoreSnapshotProcedure from "../get_auth_store_snapshot_procedure";
import * as GetBattleStateProcedure from "../get_battle_state_procedure";
import * as GetBigFishRunProcedure from "../get_big_fish_run_procedure";
import * as GetBigFishSessionProcedure from "../get_big_fish_session_procedure";
@@ -41,10 +49,12 @@ import * as GetCustomWorldAgentCardDetailProcedure from "../get_custom_world_age
import * as GetCustomWorldAgentOperationProcedure from "../get_custom_world_agent_operation_procedure";
import * as GetCustomWorldAgentSessionProcedure from "../get_custom_world_agent_session_procedure";
import * as GetCustomWorldGalleryDetailProcedure from "../get_custom_world_gallery_detail_procedure";
import * as GetCustomWorldGalleryDetailByCodeProcedure from "../get_custom_world_gallery_detail_by_code_procedure";
import * as GetCustomWorldLibraryDetailProcedure from "../get_custom_world_library_detail_procedure";
import * as GetPlayerProgressionOrDefaultProcedure from "../get_player_progression_or_default_procedure";
import * as GetProfileDashboardProcedure from "../get_profile_dashboard_procedure";
import * as GetProfilePlayStatsProcedure from "../get_profile_play_stats_procedure";
import * as GetProfileRechargeCenterProcedure from "../get_profile_recharge_center_procedure";
import * as GetPuzzleAgentSessionProcedure from "../get_puzzle_agent_session_procedure";
import * as GetPuzzleGalleryDetailProcedure from "../get_puzzle_gallery_detail_procedure";
import * as GetPuzzleRunProcedure from "../get_puzzle_run_procedure";
@@ -54,6 +64,8 @@ import * as GetRuntimeSettingOrDefaultProcedure from "../get_runtime_setting_or_
import * as GetRuntimeSnapshotProcedure from "../get_runtime_snapshot_procedure";
import * as GetStorySessionStateProcedure from "../get_story_session_state_procedure";
import * as GrantPlayerProgressionExperienceAndReturnProcedure from "../grant_player_progression_experience_and_return_procedure";
import * as ImportAuthStoreSnapshotProcedure from "../import_auth_store_snapshot_procedure";
import * as ListBigFishWorksProcedure from "../list_big_fish_works_procedure";
import * as ListCustomWorldGalleryEntriesProcedure from "../list_custom_world_gallery_entries_procedure";
import * as ListCustomWorldProfilesProcedure from "../list_custom_world_profiles_procedure";
import * as ListCustomWorldWorksProcedure from "../list_custom_world_works_procedure";
@@ -83,7 +95,9 @@ import * as SubmitPuzzleAgentMessageProcedure from "../submit_puzzle_agent_messa
import * as SwapPuzzlePiecesProcedure from "../swap_puzzle_pieces_procedure";
import * as UnpublishCustomWorldProfileAndReturnProcedure from "../unpublish_custom_world_profile_and_return_procedure";
import * as UpdatePuzzleWorkProcedure from "../update_puzzle_work_procedure";
import * as UpsertAuthStoreSnapshotProcedure from "../upsert_auth_store_snapshot_procedure";
import * as UpsertChapterProgressionAndReturnProcedure from "../upsert_chapter_progression_and_return_procedure";
import * as UpsertCustomWorldAgentOperationProgressProcedure from "../upsert_custom_world_agent_operation_progress_procedure";
import * as UpsertCustomWorldProfileAndReturnProcedure from "../upsert_custom_world_profile_and_return_procedure";
import * as UpsertNpcStateAndReturnProcedure from "../upsert_npc_state_and_return_procedure";
import * as UpsertPlatformBrowseHistoryAndReturnProcedure from "../upsert_platform_browse_history_and_return_procedure";
@@ -128,22 +142,38 @@ export type CreateBigFishSessionArgs = __Infer<typeof CreateBigFishSessionProced
export type CreateBigFishSessionResult = __Infer<typeof CreateBigFishSessionProcedure.returnType>;
export type CreateCustomWorldAgentSessionArgs = __Infer<typeof CreateCustomWorldAgentSessionProcedure.params>;
export type CreateCustomWorldAgentSessionResult = __Infer<typeof CreateCustomWorldAgentSessionProcedure.returnType>;
export type CreateProfileRechargeOrderAndReturnArgs = __Infer<typeof CreateProfileRechargeOrderAndReturnProcedure.params>;
export type CreateProfileRechargeOrderAndReturnResult = __Infer<typeof CreateProfileRechargeOrderAndReturnProcedure.returnType>;
export type CreatePuzzleAgentSessionArgs = __Infer<typeof CreatePuzzleAgentSessionProcedure.params>;
export type CreatePuzzleAgentSessionResult = __Infer<typeof CreatePuzzleAgentSessionProcedure.returnType>;
export type DeleteBigFishWorkArgs = __Infer<typeof DeleteBigFishWorkProcedure.params>;
export type DeleteBigFishWorkResult = __Infer<typeof DeleteBigFishWorkProcedure.returnType>;
export type DeleteCustomWorldAgentSessionArgs = __Infer<typeof DeleteCustomWorldAgentSessionProcedure.params>;
export type DeleteCustomWorldAgentSessionResult = __Infer<typeof DeleteCustomWorldAgentSessionProcedure.returnType>;
export type DeleteCustomWorldProfileAndReturnArgs = __Infer<typeof DeleteCustomWorldProfileAndReturnProcedure.params>;
export type DeleteCustomWorldProfileAndReturnResult = __Infer<typeof DeleteCustomWorldProfileAndReturnProcedure.returnType>;
export type DeletePuzzleWorkArgs = __Infer<typeof DeletePuzzleWorkProcedure.params>;
export type DeletePuzzleWorkResult = __Infer<typeof DeletePuzzleWorkProcedure.returnType>;
export type DeleteRuntimeSnapshotAndReturnArgs = __Infer<typeof DeleteRuntimeSnapshotAndReturnProcedure.params>;
export type DeleteRuntimeSnapshotAndReturnResult = __Infer<typeof DeleteRuntimeSnapshotAndReturnProcedure.returnType>;
export type DragPuzzlePieceOrGroupArgs = __Infer<typeof DragPuzzlePieceOrGroupProcedure.params>;
export type DragPuzzlePieceOrGroupResult = __Infer<typeof DragPuzzlePieceOrGroupProcedure.returnType>;
export type ExecuteCustomWorldAgentActionArgs = __Infer<typeof ExecuteCustomWorldAgentActionProcedure.params>;
export type ExecuteCustomWorldAgentActionResult = __Infer<typeof ExecuteCustomWorldAgentActionProcedure.returnType>;
export type ExportAuthStoreSnapshotFromTablesArgs = __Infer<typeof ExportAuthStoreSnapshotFromTablesProcedure.params>;
export type ExportAuthStoreSnapshotFromTablesResult = __Infer<typeof ExportAuthStoreSnapshotFromTablesProcedure.returnType>;
export type FailAiTaskAndReturnArgs = __Infer<typeof FailAiTaskAndReturnProcedure.params>;
export type FailAiTaskAndReturnResult = __Infer<typeof FailAiTaskAndReturnProcedure.returnType>;
export type FinalizeBigFishAgentMessageTurnArgs = __Infer<typeof FinalizeBigFishAgentMessageTurnProcedure.params>;
export type FinalizeBigFishAgentMessageTurnResult = __Infer<typeof FinalizeBigFishAgentMessageTurnProcedure.returnType>;
export type FinalizeCustomWorldAgentMessageTurnArgs = __Infer<typeof FinalizeCustomWorldAgentMessageTurnProcedure.params>;
export type FinalizeCustomWorldAgentMessageTurnResult = __Infer<typeof FinalizeCustomWorldAgentMessageTurnProcedure.returnType>;
export type FinalizePuzzleAgentMessageTurnArgs = __Infer<typeof FinalizePuzzleAgentMessageTurnProcedure.params>;
export type FinalizePuzzleAgentMessageTurnResult = __Infer<typeof FinalizePuzzleAgentMessageTurnProcedure.returnType>;
export type GenerateBigFishAssetArgs = __Infer<typeof GenerateBigFishAssetProcedure.params>;
export type GenerateBigFishAssetResult = __Infer<typeof GenerateBigFishAssetProcedure.returnType>;
export type GetAuthStoreSnapshotArgs = __Infer<typeof GetAuthStoreSnapshotProcedure.params>;
export type GetAuthStoreSnapshotResult = __Infer<typeof GetAuthStoreSnapshotProcedure.returnType>;
export type GetBattleStateArgs = __Infer<typeof GetBattleStateProcedure.params>;
export type GetBattleStateResult = __Infer<typeof GetBattleStateProcedure.returnType>;
export type GetBigFishRunArgs = __Infer<typeof GetBigFishRunProcedure.params>;
@@ -160,6 +190,8 @@ export type GetCustomWorldAgentSessionArgs = __Infer<typeof GetCustomWorldAgentS
export type GetCustomWorldAgentSessionResult = __Infer<typeof GetCustomWorldAgentSessionProcedure.returnType>;
export type GetCustomWorldGalleryDetailArgs = __Infer<typeof GetCustomWorldGalleryDetailProcedure.params>;
export type GetCustomWorldGalleryDetailResult = __Infer<typeof GetCustomWorldGalleryDetailProcedure.returnType>;
export type GetCustomWorldGalleryDetailByCodeArgs = __Infer<typeof GetCustomWorldGalleryDetailByCodeProcedure.params>;
export type GetCustomWorldGalleryDetailByCodeResult = __Infer<typeof GetCustomWorldGalleryDetailByCodeProcedure.returnType>;
export type GetCustomWorldLibraryDetailArgs = __Infer<typeof GetCustomWorldLibraryDetailProcedure.params>;
export type GetCustomWorldLibraryDetailResult = __Infer<typeof GetCustomWorldLibraryDetailProcedure.returnType>;
export type GetPlayerProgressionOrDefaultArgs = __Infer<typeof GetPlayerProgressionOrDefaultProcedure.params>;
@@ -168,6 +200,8 @@ export type GetProfileDashboardArgs = __Infer<typeof GetProfileDashboardProcedur
export type GetProfileDashboardResult = __Infer<typeof GetProfileDashboardProcedure.returnType>;
export type GetProfilePlayStatsArgs = __Infer<typeof GetProfilePlayStatsProcedure.params>;
export type GetProfilePlayStatsResult = __Infer<typeof GetProfilePlayStatsProcedure.returnType>;
export type GetProfileRechargeCenterArgs = __Infer<typeof GetProfileRechargeCenterProcedure.params>;
export type GetProfileRechargeCenterResult = __Infer<typeof GetProfileRechargeCenterProcedure.returnType>;
export type GetPuzzleAgentSessionArgs = __Infer<typeof GetPuzzleAgentSessionProcedure.params>;
export type GetPuzzleAgentSessionResult = __Infer<typeof GetPuzzleAgentSessionProcedure.returnType>;
export type GetPuzzleGalleryDetailArgs = __Infer<typeof GetPuzzleGalleryDetailProcedure.params>;
@@ -186,6 +220,10 @@ export type GetStorySessionStateArgs = __Infer<typeof GetStorySessionStateProced
export type GetStorySessionStateResult = __Infer<typeof GetStorySessionStateProcedure.returnType>;
export type GrantPlayerProgressionExperienceAndReturnArgs = __Infer<typeof GrantPlayerProgressionExperienceAndReturnProcedure.params>;
export type GrantPlayerProgressionExperienceAndReturnResult = __Infer<typeof GrantPlayerProgressionExperienceAndReturnProcedure.returnType>;
export type ImportAuthStoreSnapshotArgs = __Infer<typeof ImportAuthStoreSnapshotProcedure.params>;
export type ImportAuthStoreSnapshotResult = __Infer<typeof ImportAuthStoreSnapshotProcedure.returnType>;
export type ListBigFishWorksArgs = __Infer<typeof ListBigFishWorksProcedure.params>;
export type ListBigFishWorksResult = __Infer<typeof ListBigFishWorksProcedure.returnType>;
export type ListCustomWorldGalleryEntriesArgs = __Infer<typeof ListCustomWorldGalleryEntriesProcedure.params>;
export type ListCustomWorldGalleryEntriesResult = __Infer<typeof ListCustomWorldGalleryEntriesProcedure.returnType>;
export type ListCustomWorldProfilesArgs = __Infer<typeof ListCustomWorldProfilesProcedure.params>;
@@ -244,8 +282,12 @@ export type UnpublishCustomWorldProfileAndReturnArgs = __Infer<typeof UnpublishC
export type UnpublishCustomWorldProfileAndReturnResult = __Infer<typeof UnpublishCustomWorldProfileAndReturnProcedure.returnType>;
export type UpdatePuzzleWorkArgs = __Infer<typeof UpdatePuzzleWorkProcedure.params>;
export type UpdatePuzzleWorkResult = __Infer<typeof UpdatePuzzleWorkProcedure.returnType>;
export type UpsertAuthStoreSnapshotArgs = __Infer<typeof UpsertAuthStoreSnapshotProcedure.params>;
export type UpsertAuthStoreSnapshotResult = __Infer<typeof UpsertAuthStoreSnapshotProcedure.returnType>;
export type UpsertChapterProgressionAndReturnArgs = __Infer<typeof UpsertChapterProgressionAndReturnProcedure.params>;
export type UpsertChapterProgressionAndReturnResult = __Infer<typeof UpsertChapterProgressionAndReturnProcedure.returnType>;
export type UpsertCustomWorldAgentOperationProgressArgs = __Infer<typeof UpsertCustomWorldAgentOperationProgressProcedure.params>;
export type UpsertCustomWorldAgentOperationProgressResult = __Infer<typeof UpsertCustomWorldAgentOperationProgressProcedure.returnType>;
export type UpsertCustomWorldProfileAndReturnArgs = __Infer<typeof UpsertCustomWorldProfileAndReturnProcedure.params>;
export type UpsertCustomWorldProfileAndReturnResult = __Infer<typeof UpsertCustomWorldProfileAndReturnProcedure.returnType>;
export type UpsertNpcStateAndReturnArgs = __Infer<typeof UpsertNpcStateAndReturnProcedure.params>;

View File

@@ -0,0 +1,23 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
import {
AuthStoreSnapshotProcedureResult,
AuthStoreSnapshotUpsertInput,
} from "./types";
export const params = {
get input() {
return AuthStoreSnapshotUpsertInput;
},
};
export const returnType = AuthStoreSnapshotProcedureResult

View File

@@ -0,0 +1,23 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
import {
CustomWorldAgentOperationProcedureResult,
CustomWorldAgentOperationProgressInput,
} from "./types";
export const params = {
get input() {
return CustomWorldAgentOperationProgressInput;
},
};
export const returnType = CustomWorldAgentOperationProcedureResult