fix: 完善作品号展示与复制入口
This commit is contained in:
@@ -67,7 +67,10 @@ import {
|
||||
getPuzzleAgentSession,
|
||||
streamPuzzleAgentMessage,
|
||||
} from '../../services/puzzle-agent';
|
||||
import { getPuzzleGalleryDetail, listPuzzleGallery } from '../../services/puzzle-gallery';
|
||||
import {
|
||||
getPuzzleGalleryDetail,
|
||||
listPuzzleGallery,
|
||||
} from '../../services/puzzle-gallery';
|
||||
import { advanceLocalPuzzleNextLevel } from '../../services/puzzle-runtime';
|
||||
import {
|
||||
dragLocalPuzzlePiece,
|
||||
@@ -75,6 +78,7 @@ import {
|
||||
swapLocalPuzzlePieces,
|
||||
} from '../../services/puzzle-runtime/puzzleLocalRuntime';
|
||||
import { deletePuzzleWork, listPuzzleWorks } from '../../services/puzzle-works';
|
||||
import { isSamePuzzlePublicWorkCode } from '../../services/publicWorkCode';
|
||||
import { deleteRpgCreationAgentSession } from '../../services/rpg-creation';
|
||||
import { rpgCreationPreviewAdapter } from '../../services/rpg-creation/rpgCreationPreviewAdapter';
|
||||
import { deleteRpgEntryWorldProfile } from '../../services/rpg-entry';
|
||||
@@ -267,8 +271,7 @@ function buildAgentResultPublishGateView(
|
||||
|
||||
const blockers = fallbackBlockers
|
||||
.filter(
|
||||
(entry) =>
|
||||
!isAgentResultStructuralBlockerResolved(profile, entry.code),
|
||||
(entry) => !isAgentResultStructuralBlockerResolved(profile, entry.code),
|
||||
)
|
||||
.map((entry) => entry.message);
|
||||
|
||||
@@ -367,7 +370,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
const [isPuzzleNextLevelGenerating, setIsPuzzleNextLevelGenerating] =
|
||||
useState(false);
|
||||
const [isSearchingPublicCode, setIsSearchingPublicCode] = useState(false);
|
||||
const [publicSearchError, setPublicSearchError] = useState<string | null>(null);
|
||||
const [publicSearchError, setPublicSearchError] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [searchedPublicUser, setSearchedPublicUser] =
|
||||
useState<PublicUserSummary | null>(null);
|
||||
const [deletingCreationWorkId, setDeletingCreationWorkId] = useState<
|
||||
@@ -491,9 +496,11 @@ export function PlatformEntryFlowShellImpl({
|
||||
agentSession: sessionController.agentSession,
|
||||
handleCustomWorldSelect,
|
||||
executePublishWorld: async () => {
|
||||
const latestSession = await autosaveCoordinator.executeAgentActionAndWait({
|
||||
action: 'publish_world',
|
||||
});
|
||||
const latestSession = await autosaveCoordinator.executeAgentActionAndWait(
|
||||
{
|
||||
action: 'publish_world',
|
||||
},
|
||||
);
|
||||
// 发布动作会在后端同步 gallery 投影;前端发布完成后立即刷新首页/分类页共用的公开作品列表。
|
||||
await Promise.allSettled([
|
||||
platformBootstrap.refreshPublishedGallery(),
|
||||
@@ -551,18 +558,15 @@ export function PlatformEntryFlowShellImpl({
|
||||
return '服务端预览';
|
||||
}, [agentResultPreview]);
|
||||
|
||||
const featuredGalleryEntries = useMemo(
|
||||
() => {
|
||||
const puzzlePublicEntries = puzzleGalleryEntries.map(
|
||||
mapPuzzleWorkToPlatformGalleryCard,
|
||||
);
|
||||
return mergePlatformPublicGalleryEntries(
|
||||
platformBootstrap.publishedGalleryEntries,
|
||||
puzzlePublicEntries,
|
||||
).slice(0, 6);
|
||||
},
|
||||
[platformBootstrap.publishedGalleryEntries, puzzleGalleryEntries],
|
||||
);
|
||||
const featuredGalleryEntries = useMemo(() => {
|
||||
const puzzlePublicEntries = puzzleGalleryEntries.map(
|
||||
mapPuzzleWorkToPlatformGalleryCard,
|
||||
);
|
||||
return mergePlatformPublicGalleryEntries(
|
||||
platformBootstrap.publishedGalleryEntries,
|
||||
puzzlePublicEntries,
|
||||
).slice(0, 6);
|
||||
}, [platformBootstrap.publishedGalleryEntries, puzzleGalleryEntries]);
|
||||
const latestGalleryEntries = useMemo(
|
||||
() =>
|
||||
mergePlatformPublicGalleryEntries(
|
||||
@@ -608,86 +612,6 @@ export function PlatformEntryFlowShellImpl({
|
||||
[authUi],
|
||||
);
|
||||
|
||||
const handlePublicCodeSearch = useCallback(
|
||||
async (keyword: string) => {
|
||||
const normalizedKeyword = keyword.trim();
|
||||
if (!normalizedKeyword) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSearchingPublicCode(true);
|
||||
setPublicSearchError(null);
|
||||
setSearchedPublicUser(null);
|
||||
|
||||
const upperKeyword = normalizedKeyword.toUpperCase();
|
||||
const shouldSearchUserIdFirst = /^user[_-][a-z0-9_-]+$/iu.test(normalizedKeyword);
|
||||
const shouldSearchWorkFirst =
|
||||
!shouldSearchUserIdFirst &&
|
||||
(upperKeyword.startsWith('CW') || /^\d{1,8}$/u.test(normalizedKeyword));
|
||||
const shouldSearchUserFirst =
|
||||
shouldSearchUserIdFirst || upperKeyword.startsWith('SY') || !shouldSearchWorkFirst;
|
||||
|
||||
const tryOpenGalleryEntry = async () => {
|
||||
const entry = await getRpgEntryWorldGalleryDetailByCode(normalizedKeyword);
|
||||
await detailNavigation.openGalleryDetail({
|
||||
ownerUserId: entry.ownerUserId,
|
||||
profileId: entry.profileId,
|
||||
publicWorkCode: entry.publicWorkCode,
|
||||
authorPublicUserCode: entry.authorPublicUserCode,
|
||||
visibility: 'published',
|
||||
publishedAt: entry.publishedAt,
|
||||
updatedAt: entry.updatedAt,
|
||||
authorDisplayName: entry.authorDisplayName,
|
||||
worldName: entry.worldName,
|
||||
subtitle: entry.subtitle,
|
||||
summaryText: entry.summaryText,
|
||||
coverImageSrc: entry.coverImageSrc,
|
||||
themeMode: entry.themeMode,
|
||||
playableNpcCount: entry.playableNpcCount,
|
||||
landmarkCount: entry.landmarkCount,
|
||||
} satisfies CustomWorldGalleryCard);
|
||||
};
|
||||
|
||||
try {
|
||||
if (shouldSearchUserIdFirst) {
|
||||
const user = await getPublicAuthUserById(normalizedKeyword);
|
||||
setSearchedPublicUser(user);
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldSearchWorkFirst) {
|
||||
try {
|
||||
await tryOpenGalleryEntry();
|
||||
return;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (shouldSearchUserFirst) {
|
||||
try {
|
||||
const user = await getPublicAuthUserByCode(normalizedKeyword);
|
||||
setSearchedPublicUser(user);
|
||||
return;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (!shouldSearchWorkFirst) {
|
||||
await tryOpenGalleryEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await getPublicAuthUserByCode(normalizedKeyword);
|
||||
setSearchedPublicUser(user);
|
||||
} catch (error) {
|
||||
setPublicSearchError(
|
||||
resolveRpgCreationErrorMessage(error, '未找到对应的叙世号或作品号。'),
|
||||
);
|
||||
} finally {
|
||||
setIsSearchingPublicCode(false);
|
||||
}
|
||||
},
|
||||
[detailNavigation],
|
||||
);
|
||||
|
||||
const prepareCreationLaunch = useCallback(() => {
|
||||
if (sessionController.isCreatingAgentSession) {
|
||||
return false;
|
||||
@@ -748,9 +672,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
return galleryResponse.items;
|
||||
} catch (error) {
|
||||
setPuzzleGalleryEntries([]);
|
||||
setPuzzleError(
|
||||
resolvePuzzleErrorMessage(error, '读取拼图广场失败。'),
|
||||
);
|
||||
setPuzzleError(resolvePuzzleErrorMessage(error, '读取拼图广场失败。'));
|
||||
return [];
|
||||
}
|
||||
}, [resolvePuzzleErrorMessage]);
|
||||
@@ -835,7 +757,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
{ session: PuzzleAgentSessionSnapshot },
|
||||
SendPuzzleAgentMessageRequest,
|
||||
PuzzleAgentActionRequest,
|
||||
{ operation: PuzzleAgentOperationRecord; session: PuzzleAgentSessionSnapshot }
|
||||
{
|
||||
operation: PuzzleAgentOperationRecord;
|
||||
session: PuzzleAgentSessionSnapshot;
|
||||
}
|
||||
>({
|
||||
client: {
|
||||
createSession: createPuzzleAgentSession,
|
||||
@@ -1165,11 +1090,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
);
|
||||
|
||||
const dragPuzzlePiece = useCallback(
|
||||
(payload: {
|
||||
pieceId: string;
|
||||
targetRow: number;
|
||||
targetCol: number;
|
||||
}) => {
|
||||
(payload: { pieceId: string; targetRow: number; targetCol: number }) => {
|
||||
if (!puzzleRun || isPuzzleBusy) {
|
||||
return;
|
||||
}
|
||||
@@ -1198,7 +1119,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
const { run } = await advanceLocalPuzzleNextLevel({
|
||||
run: puzzleRun,
|
||||
sourceSessionId:
|
||||
selectedPuzzleDetail?.sourceSessionId ?? puzzleSession?.sessionId ?? null,
|
||||
selectedPuzzleDetail?.sourceSessionId ??
|
||||
puzzleSession?.sessionId ??
|
||||
null,
|
||||
});
|
||||
setPuzzleRun(run);
|
||||
} catch (error) {
|
||||
@@ -1293,7 +1216,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
(entry) => entry.profileId === work.profileId,
|
||||
);
|
||||
if (!matchedEntry) {
|
||||
platformBootstrap.setPlatformError('未找到可体验的作品,请刷新后重试。');
|
||||
platformBootstrap.setPlatformError(
|
||||
'未找到可体验的作品,请刷新后重试。',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1362,10 +1287,14 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
const deleteTask =
|
||||
work.sourceType === 'published_profile' && work.profileId
|
||||
? deleteRpgEntryWorldProfile(work.profileId).then(async (entries) => {
|
||||
platformBootstrap.setSavedCustomWorldEntries(entries);
|
||||
await platformBootstrap.refreshCustomWorldWorks().catch(() => []);
|
||||
})
|
||||
? deleteRpgEntryWorldProfile(work.profileId).then(
|
||||
async (entries) => {
|
||||
platformBootstrap.setSavedCustomWorldEntries(entries);
|
||||
await platformBootstrap
|
||||
.refreshCustomWorldWorks()
|
||||
.catch(() => []);
|
||||
},
|
||||
)
|
||||
: work.sourceType === 'agent_session' && work.sessionId
|
||||
? deleteRpgCreationAgentSession(work.sessionId).then((items) => {
|
||||
platformBootstrap.setCustomWorldWorkEntries(items);
|
||||
@@ -1446,7 +1375,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
void refreshPuzzleGallery();
|
||||
})
|
||||
.catch((error) => {
|
||||
setPuzzleError(resolvePuzzleErrorMessage(error, '删除拼图作品失败。'));
|
||||
setPuzzleError(
|
||||
resolvePuzzleErrorMessage(error, '删除拼图作品失败。'),
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
setDeletingCreationWorkId(null);
|
||||
@@ -1485,12 +1416,136 @@ export function PlatformEntryFlowShellImpl({
|
||||
setPuzzleOperation(null);
|
||||
setPuzzleRun(null);
|
||||
setSelectedPuzzleDetail(null);
|
||||
const restoredSession = await puzzleFlow.restoreDraft(item.sourceSessionId);
|
||||
if (!item.sourceSessionId?.trim()) {
|
||||
if (item.publicationStatus === 'published') {
|
||||
await openPuzzleDetail(item.profileId);
|
||||
return;
|
||||
}
|
||||
|
||||
setPuzzleError('这份拼图草稿缺少会话信息,请重新开始创作。');
|
||||
return;
|
||||
}
|
||||
|
||||
const restoredSession = await puzzleFlow.restoreDraft(
|
||||
item.sourceSessionId,
|
||||
);
|
||||
if (!restoredSession) {
|
||||
await refreshPuzzleShelf().catch(() => undefined);
|
||||
}
|
||||
},
|
||||
[puzzleFlow, refreshPuzzleShelf],
|
||||
[openPuzzleDetail, puzzleFlow, refreshPuzzleShelf, setPuzzleError],
|
||||
);
|
||||
|
||||
const handlePublicCodeSearch = useCallback(
|
||||
async (keyword: string) => {
|
||||
const normalizedKeyword = keyword.trim();
|
||||
if (!normalizedKeyword) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSearchingPublicCode(true);
|
||||
setPublicSearchError(null);
|
||||
setSearchedPublicUser(null);
|
||||
|
||||
const upperKeyword = normalizedKeyword.toUpperCase();
|
||||
const shouldSearchUserIdFirst = /^user[_-][a-z0-9_-]+$/iu.test(
|
||||
normalizedKeyword,
|
||||
);
|
||||
const shouldSearchPuzzleFirst = upperKeyword.startsWith('PZ');
|
||||
const shouldSearchWorkFirst =
|
||||
!shouldSearchUserIdFirst &&
|
||||
!shouldSearchPuzzleFirst &&
|
||||
(upperKeyword.startsWith('CW') || /^\d{1,8}$/u.test(normalizedKeyword));
|
||||
const shouldSearchUserFirst =
|
||||
shouldSearchUserIdFirst ||
|
||||
upperKeyword.startsWith('SY') ||
|
||||
(!shouldSearchWorkFirst && !shouldSearchPuzzleFirst);
|
||||
|
||||
const tryOpenGalleryEntry = async () => {
|
||||
const entry =
|
||||
await getRpgEntryWorldGalleryDetailByCode(normalizedKeyword);
|
||||
await detailNavigation.openGalleryDetail({
|
||||
ownerUserId: entry.ownerUserId,
|
||||
profileId: entry.profileId,
|
||||
publicWorkCode: entry.publicWorkCode,
|
||||
authorPublicUserCode: entry.authorPublicUserCode,
|
||||
visibility: 'published',
|
||||
publishedAt: entry.publishedAt,
|
||||
updatedAt: entry.updatedAt,
|
||||
authorDisplayName: entry.authorDisplayName,
|
||||
worldName: entry.worldName,
|
||||
subtitle: entry.subtitle,
|
||||
summaryText: entry.summaryText,
|
||||
coverImageSrc: entry.coverImageSrc,
|
||||
themeMode: entry.themeMode,
|
||||
playableNpcCount: entry.playableNpcCount,
|
||||
landmarkCount: entry.landmarkCount,
|
||||
} satisfies CustomWorldGalleryCard);
|
||||
};
|
||||
const tryOpenPuzzleGalleryEntry = async () => {
|
||||
const entries =
|
||||
puzzleGalleryEntries.length > 0
|
||||
? puzzleGalleryEntries
|
||||
: await refreshPuzzleGallery();
|
||||
const matchedEntry = entries.find((entry) =>
|
||||
isSamePuzzlePublicWorkCode(normalizedKeyword, entry.profileId),
|
||||
);
|
||||
|
||||
if (!matchedEntry) {
|
||||
throw new Error('未找到拼图作品。');
|
||||
}
|
||||
|
||||
await openPuzzleDetail(matchedEntry.profileId);
|
||||
};
|
||||
|
||||
try {
|
||||
if (shouldSearchUserIdFirst) {
|
||||
const user = await getPublicAuthUserById(normalizedKeyword);
|
||||
setSearchedPublicUser(user);
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldSearchPuzzleFirst) {
|
||||
await tryOpenPuzzleGalleryEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldSearchWorkFirst) {
|
||||
try {
|
||||
await tryOpenGalleryEntry();
|
||||
return;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (shouldSearchUserFirst) {
|
||||
try {
|
||||
const user = await getPublicAuthUserByCode(normalizedKeyword);
|
||||
setSearchedPublicUser(user);
|
||||
return;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (!shouldSearchWorkFirst) {
|
||||
await tryOpenGalleryEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await getPublicAuthUserByCode(normalizedKeyword);
|
||||
setSearchedPublicUser(user);
|
||||
} catch (error) {
|
||||
setPublicSearchError(
|
||||
resolveRpgCreationErrorMessage(error, '未找到对应的叙世号或作品号。'),
|
||||
);
|
||||
} finally {
|
||||
setIsSearchingPublicCode(false);
|
||||
}
|
||||
},
|
||||
[
|
||||
detailNavigation,
|
||||
openPuzzleDetail,
|
||||
puzzleGalleryEntries,
|
||||
refreshPuzzleGallery,
|
||||
],
|
||||
);
|
||||
|
||||
const openBigFishDraft = useCallback(
|
||||
@@ -1632,6 +1687,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
onExperienceRpg={(item) => {
|
||||
handleExperienceRpgWork(item);
|
||||
}}
|
||||
rpgLibraryEntries={platformBootstrap.savedCustomWorldEntries}
|
||||
bigFishItems={bigFishWorks}
|
||||
onOpenBigFishDetail={(item) => {
|
||||
runProtectedAction(() => {
|
||||
@@ -1649,11 +1705,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
puzzleItems={puzzleWorks}
|
||||
onOpenPuzzleDetail={(item) => {
|
||||
runProtectedAction(() => {
|
||||
if (item.publicationStatus === 'draft') {
|
||||
void openPuzzleDraft(item);
|
||||
return;
|
||||
}
|
||||
void openPuzzleDetail(item.profileId);
|
||||
void openPuzzleDraft(item);
|
||||
});
|
||||
}}
|
||||
onExperiencePuzzle={(profileId) => {
|
||||
@@ -1894,13 +1946,17 @@ export function PlatformEntryFlowShellImpl({
|
||||
className="flex h-full min-h-0 flex-col"
|
||||
>
|
||||
<Suspense
|
||||
fallback={<LazyPanelFallback label="正在加载大鱼吃小鱼生成面板..." />}
|
||||
fallback={
|
||||
<LazyPanelFallback label="正在加载大鱼吃小鱼生成面板..." />
|
||||
}
|
||||
>
|
||||
<CustomWorldGenerationView
|
||||
settingText={
|
||||
bigFishSession?.lastAssistantReply ?? '正在整理当前玩法草稿。'
|
||||
}
|
||||
anchorEntries={buildBigFishGenerationAnchorEntries(bigFishSession)}
|
||||
anchorEntries={buildBigFishGenerationAnchorEntries(
|
||||
bigFishSession,
|
||||
)}
|
||||
progress={buildMiniGameDraftGenerationProgress(
|
||||
bigFishGenerationState,
|
||||
)}
|
||||
@@ -1911,7 +1967,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
setSelectionStage('big-fish-agent-workspace');
|
||||
}}
|
||||
onRetry={() => {
|
||||
void executeBigFishAction({ action: 'big_fish_compile_draft' });
|
||||
void executeBigFishAction({
|
||||
action: 'big_fish_compile_draft',
|
||||
});
|
||||
}}
|
||||
onInterrupt={undefined}
|
||||
backLabel="返回创作中心"
|
||||
@@ -2026,7 +2084,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
settingText={
|
||||
puzzleSession?.lastAssistantReply ?? '正在整理当前拼图草稿。'
|
||||
}
|
||||
anchorEntries={buildPuzzleGenerationAnchorEntries(puzzleSession)}
|
||||
anchorEntries={buildPuzzleGenerationAnchorEntries(
|
||||
puzzleSession,
|
||||
)}
|
||||
progress={buildMiniGameDraftGenerationProgress(
|
||||
puzzleGenerationState,
|
||||
)}
|
||||
@@ -2093,6 +2153,16 @@ export function PlatformEntryFlowShellImpl({
|
||||
enterCreateTab();
|
||||
setSelectionStage('platform');
|
||||
}}
|
||||
onEdit={
|
||||
selectedPuzzleDetail.ownerUserId === authUi?.user?.id &&
|
||||
Boolean(selectedPuzzleDetail.sourceSessionId?.trim())
|
||||
? () => {
|
||||
runProtectedAction(() => {
|
||||
void openPuzzleDraft(selectedPuzzleDetail);
|
||||
});
|
||||
}
|
||||
: null
|
||||
}
|
||||
onStartGame={() => {
|
||||
void startPuzzleRunFromProfile(selectedPuzzleDetail.profileId);
|
||||
}}
|
||||
@@ -2271,15 +2341,17 @@ export function PlatformEntryFlowShellImpl({
|
||||
? 'generate_landmarks'
|
||||
: 'generate_characters';
|
||||
const latestSession =
|
||||
await autosaveCoordinator.executeAgentActionAndWait({
|
||||
action,
|
||||
count: 1,
|
||||
...(kind === 'playable'
|
||||
? { roleType: 'playable' as const }
|
||||
: kind === 'story'
|
||||
? { roleType: 'story' as const }
|
||||
: {}),
|
||||
});
|
||||
await autosaveCoordinator.executeAgentActionAndWait(
|
||||
{
|
||||
action,
|
||||
count: 1,
|
||||
...(kind === 'playable'
|
||||
? { roleType: 'playable' as const }
|
||||
: kind === 'story'
|
||||
? { roleType: 'story' as const }
|
||||
: {}),
|
||||
},
|
||||
);
|
||||
const latestProfile = latestSession
|
||||
? rpgCreationPreviewAdapter.buildPreviewFromSession(
|
||||
latestSession,
|
||||
|
||||
Reference in New Issue
Block a user