1
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}`}
|
||||
>
|
||||
|
||||
@@ -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="当前筛选下没有内容" />
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
247
src/components/custom-world-home/creationWorkShelf.ts
Normal file
247
src/components/custom-world-home/creationWorkShelf.ts
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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()}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { WorldFoundationEditor as default } from './RpgCreationEntityEditorShared';
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
176
src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx
Normal file
176
src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx
Normal 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));
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
|
||||
278
src/components/rpg-entry/useRpgEntryAgentDraftRestore.test.tsx
Normal file
278
src/components/rpg-entry/useRpgEntryAgentDraftRestore.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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`}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -660,7 +660,8 @@ body {
|
||||
);
|
||||
}
|
||||
|
||||
.platform-surface > * {
|
||||
/* 背景图和遮罩必须保持绝对定位,避免 banner 图进入普通流后撑开首页布局。 */
|
||||
.platform-surface > :not(.absolute) {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
48
src/routing/appPageRoutes.test.ts
Normal file
48
src/routing/appPageRoutes.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
69
src/routing/appPageRoutes.ts
Normal file
69
src/routing/appPageRoutes.ts
Normal 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);
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
165
src/services/creation-agent/creationAgentClientFactory.ts
Normal file
165
src/services/creation-agent/creationAgentClientFactory.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
82
src/services/creation-agent/creationAgentDocumentInput.ts
Normal file
82
src/services/creation-agent/creationAgentDocumentInput.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './creationAgentClientFactory';
|
||||
export * from './creationAgentChat';
|
||||
export * from './creationAgentDocumentInput';
|
||||
export * from './creationAgentProgress';
|
||||
export * from './creationAgentSse';
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
350
src/services/customWorldFoundationEntries.ts
Normal file
350
src/services/customWorldFoundationEntries.ts
Normal 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(';'),
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
@@ -11,8 +11,10 @@ export {
|
||||
} from './rpgEntryLibraryClient';
|
||||
export {
|
||||
clearRpgProfileBrowseHistory,
|
||||
createRpgProfileRechargeOrder,
|
||||
getRpgProfileDashboard,
|
||||
getRpgProfilePlayStats,
|
||||
getRpgProfileRechargeCenter,
|
||||
getRpgProfileSettings,
|
||||
getRpgProfileWalletLedger,
|
||||
listRpgProfileBrowseHistory,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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(),
|
||||
|
||||
23
src/spacetime/generated/delete_big_fish_work_procedure.ts
Normal file
23
src/spacetime/generated/delete_big_fish_work_procedure.ts
Normal 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
|
||||
@@ -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
|
||||
23
src/spacetime/generated/delete_puzzle_work_procedure.ts
Normal file
23
src/spacetime/generated/delete_puzzle_work_procedure.ts
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
19
src/spacetime/generated/get_auth_store_snapshot_procedure.ts
Normal file
19
src/spacetime/generated/get_auth_store_snapshot_procedure.ts
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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),
|
||||
|
||||
23
src/spacetime/generated/list_big_fish_works_procedure.ts
Normal file
23
src/spacetime/generated/list_big_fish_works_procedure.ts
Normal 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
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
} from "spacetimedb";
|
||||
|
||||
import {
|
||||
CustomWorldWorksListInput,
|
||||
CustomWorldWorksListResult,
|
||||
CustomWorldWorksListInput,
|
||||
} from "./types";
|
||||
|
||||
export const params = {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user