fix: 完善作品号展示与复制入口

This commit is contained in:
2026-04-26 14:13:34 +08:00
parent 0a0f3f1bd8
commit 874e10383b
17 changed files with 951 additions and 253 deletions

View File

@@ -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,